Date picker

Summary

A calendar popover anchored to a text input. Clicking the input opens a 6×7 month grid; the user picks a day and the input's value is filled in ISO YYYY-MM-DD format. Hand-rolled: no jQuery UI, no third-party calendar library. Auto-binds on DOMContentLoaded for any input with the .neura-date-picker-input marker class.

Fully keyboard-accessible: the input stays typeable while the popover is open; moves focus into the day grid, arrows move by day/week, Home/End jump to week edges, PageUp/PageDown change month (Shift: year), Enter selects, and Esc closes and returns focus to the input. The popover also closes on an outside click and when the page scrolls.

When to use

PatternUse it for
Auto-init
.neura-date-picker-input
Static forms: add the marker class to a text input and the picker binds on load.
Imperative
Neura.datePicker(input, opts)
Dynamic forms or when you need to listen for change events / set values from JS.
US convention
firstDayOfWeek: 0
Sunday-first calendar layout. Default is Monday-first (ISO 8601).

Examples

Click any input to open the calendar popover. Each demo binds with a different option set so you can verify min/max bounds, locales, time picker, and the paired-range behaviour.

Default - Monday-first

Click to pick from a calendar.

With initial value

US convention - Sunday-first

First day of week: Sunday.

Listening for change

Last picked: (none)

Min / max bounds

Days outside the current calendar year render disabled.
Rolling window - min is today minus 30 days, max is today.

Locale - French

Month + day labels via Intl.DateTimeFormat('fr-FR').

Date + time

Pick a day, then a 24h time. Step: 15 min.

Date range

Picking a start sets the end's minimum and vice versa. Selected: (none)

Keyboard navigation

All pickers support keyboard nav when the popover is open and focus is in the day grid:

KeyAction
/ Previous / next day
/ Same day previous / next week
Home / EndStart / end of the focused week
PgUp / PgDnPrevious / next month
Shift+PgUp / PgDnPrevious / next year
Enter / SpaceSelect the focused day
EscClose the popover

HTML

Auto-init: add the marker class on any <input type="text">. No JS needed:

<input type="text"
       class="neura-field-text neura-date-picker-input"
       placeholder="YYYY-MM-DD">

Pre-populate the input (value must be ISO YYYY-MM-DD):

<input type="text"
       class="neura-field-text neura-date-picker-input"
       value="2024-03-12">

Inside a Neura form group:

<form class="neura-form">
  <div class="neura-field-group">
    <label for="dob">Birth date</label>
    <input type="text" id="dob"
           class="neura-field-text neura-date-picker-input"
           placeholder="YYYY-MM-DD">
    <div class="neura-field-description">Click to pick from a calendar.</div>
  </div>
</form>

CSS classes

ClassEffect
.neura-date-picker-inputMarker on the text input. Triggers auto-init binding.
.neura-date-pickerPopover container (rendered by JS, not authored manually).
.neura-date-picker-headerTop row: month nav + title.
.neura-date-picker-prev / .neura-date-picker-nextPrev / next month buttons.
.neura-date-picker-title"March 2025" title between the nav buttons.
.neura-date-picker-gridThe 6×7 day grid <table>.
.neura-date-picker-dayEach clickable day cell. Carries its date as data-date="YYYY-MM-DD".
.is-today / .is-selected / .is-other-monthState classes on day cells: today's date, the selected value, and leading/trailing days of the neighbouring months.
.neura-date-picker-timeTime row (rendered only with time: true); holds the .neura-date-picker-hour and .neura-date-picker-minute selects.
.neura-date-picker-footerToday / Clear buttons.

JavaScript

Constructor + options

MemberDescription
Neura.datePicker(input, opts?)Get or create the singleton picker for an input element / selector.
opts.firstDayOfWeek0 = Sunday, 1 = Monday (default). When this option is not set, falls back to the first day of the effective locale: opts.locale if supplied, otherwise the global Neura.i18n locale.
opts.localeBCP-47 string (e.g. 'fr-FR', 'ja-JP'). Localizes month and day names via Intl.DateTimeFormat.
opts.minDate or ISO string. Days before this render disabled and ignore clicks.
opts.maxDate or ISO string. Days after this render disabled and ignore clicks.
opts.timetrue to enable a 24h HH:MM picker row. Output format becomes YYYY-MM-DDTHH:MM.
opts.timeStepMinute granularity for the time row. Default 15.
opts.formatOutput format is fixed by time: ISO YYYY-MM-DD, or YYYY-MM-DDTHH:MM when time is set. Custom format strings are not supported in v0.1.

Instance methods

MemberDescription
.show()Open the calendar popover. Never steals focus; the input stays typeable.
.hide()Close it.
.focusGrid()Move keyboard focus to the focused day cell in the grid (what in the input does).
.setValue(date)Set the value. Accepts a Date, an ISO string, or null to clear.
.getValue()Returns the current value as a Date instance, or null when empty.
.setMin(date)Update the minimum bound at runtime. Re-renders the grid.
.setMax(date)Update the maximum bound at runtime.
.on('change', fn)Fires when the user picks a date or clears the input. e.detail is { value, date }; value is '' when cleared.
.off('change', fn)Unsubscribe.
.destroy()Tear down listeners and remove the popover from the DOM.

All methods return the instance, so calls chain. The underlying DOM event is a bubbling neura-date-picker-change CustomEvent dispatched on the input, usable directly with addEventListener for delegated listening.

Date range API

MemberDescription
Neura.dateRange(startInput, endInput, opts?)Pair two pickers. Start's value becomes end's min, end's becomes start's max. Returns a façade with the members below.
.start / .endThe two underlying DatePicker instances. Use them for any per-side operation.
.getValue()Returns { start: Date | null, end: Date | null }.
.on('change', fn)Fires when either side changes. e.detail is { start, end }. (DOM event: a bubbling neura-date-range-change CustomEvent on the start input.)
.off('change', fn)Unsubscribe.
.destroy()Destroy both inner pickers and remove listeners.

Listen for date changes:

<input type="text" id="dob" class="neura-field-text neura-date-picker-input" placeholder="YYYY-MM-DD">

<script>
  Neura.datePicker('#dob').on('change', (e) => {
    console.log('picked:', e.detail.value);  // 'YYYY-MM-DD' or '' when cleared
    console.log('as Date:', e.detail.date);  // Date object or undefined
  });
</script>

US convention (Sunday-first):

Neura.datePicker('#meeting', { firstDayOfWeek: 0 });

Set / read the value programmatically:

const dp = Neura.datePicker('#dob');

// Set from a Date object
dp.setValue(new Date(2025, 2, 15));   // → input becomes "2025-03-15"

// Set from an ISO string
dp.setValue('2025-03-15');

// Read back
const date = dp.getValue();            // → Date instance or null

// Clear
dp.setValue(null);

Re-bind a date picker after rendering an input dynamically:

// Fields rendered after page load won't have been touched by auto-init.
// Re-scan the document, or just instantiate explicitly:
import('@neura/js/components/date-picker.js').then((m) => m.autoInit());

// Or, for a single known input:
Neura.datePicker(document.querySelector('#new-row .neura-date-picker-input'));

For custom validation on submit, read the parsed value:

document.getElementById('signup').addEventListener('submit', (e) => {
  const date = Neura.datePicker('#dob').getValue();
  if (!date) {
    e.preventDefault();
    alert('Please pick a date');
    return;
  }
  if (date.getFullYear() < 1900) {
    e.preventDefault();
    alert('Invalid year');
  }
});

Min / max bounds

Set min / max at construction time. Both accept a Date or an ISO string:

Neura.datePicker('#dob', {
  min: '1900-01-01',  // ISO string
  max: new Date(),    // Date instance - not in the future
});

For relative bounds (e.g. only the last 30 days, or the next 90 days), compute the Date with arithmetic:

// Today minus 30 days, up to today
const min = new Date();
min.setDate(min.getDate() - 30);
Neura.datePicker('#expense-date', { min, max: new Date() });

// Today through 90 days from now
const max = new Date();
max.setDate(max.getDate() + 90);
Neura.datePicker('#delivery', { min: new Date(), max });

Note: setDate() handles month and year roll-over automatically, so subtracting 30 days from March 5 correctly lands in February (or January, etc.).

Update bounds at runtime, useful when one input depends on another:

const arrival = Neura.datePicker('#arrival');
const departure = Neura.datePicker('#departure');

arrival.on('change', (e) => {
  departure.setMin(e.detail.date || null);
});

Out-of-range days render with the native disabled attribute and aria-disabled="true". Prev/next month buttons disable themselves automatically when the entire neighbouring month is out of range, and the Today footer button is a no-op while today is out of range.

Locales

Pass any BCP-47 locale string. Neura uses Intl.DateTimeFormat for month and day names, and Intl.Locale().getWeekInfo() for the locale's first day of week (with Monday as fallback for older browsers).

Neura.datePicker('#dob', { locale: 'fr-FR' });   // janvier · lun., mar., …
Neura.datePicker('#dob', { locale: 'ja-JP' });   // 1月 · 月, 火, …
Neura.datePicker('#dob', { locale: 'en-US' });   // English, Sunday-first

Override the locale's first-day-of-week:

// Use French labels but force Monday-first regardless of the locale's default:
Neura.datePicker('#dob', { locale: 'fr-FR', firstDayOfWeek: 1 });

Date + time

Set time: true to enable a 24h HH:MM picker row below the calendar. The output format becomes ISO-8601 YYYY-MM-DDTHH:MM and the popover stays open after a day is picked so the user can still tweak the time:

Neura.datePicker('#reservation', {
  time: true,
  timeStep: 30,    // half-hour granularity (default is 15)
});

When parsing input values, both YYYY-MM-DD and YYYY-MM-DDTHH:MM are accepted, so an input pre-filled with a date-only value works fine and the time defaults to midnight.

Date range

Pair two date inputs so picking the start automatically constrains the end (and vice versa):

<input type="text" id="trip-start" class="neura-field-text">
<input type="text" id="trip-end"   class="neura-field-text">

<script>
  const range = Neura.dateRange('#trip-start', '#trip-end');

  range.on('change', (e) => {
    console.log('range:', e.detail.start, '→', e.detail.end);
  });
</script>

Both inputs receive their own date-picker instance, accessible via range.start and range.end if you need to call show(), listen to per-side events, etc. Pass opts to apply the same configuration (locale, time, outer min/max) to both pickers.

Keyboard navigation

When the popover is open and focus is in the grid, the arrow keys move through days, PgUp/PgDn change month, Home/End jump within the focused week, Enter selects, and Esc closes. The full mapping is in the Examples section above. Out-of-range days are skipped; the focus halts at a disabled cell rather than landing on it. Under dir="rtl" the / keys follow visual direction ( moves forward a day); the other keys stay temporal.

AUI compatibility

The legacy AJS.DatePicker entry point is routed through the shim to Neura.datePicker, so existing Velocity templates and plugins that call AJS.DatePicker(input, opts) continue to work. Inputs marked with the legacy .aui-date-picker class are picked up by the shim's JS auto-init and bound to the Neura date picker.