# WDatePicker A utility-first date picker component built on [WPopover](./w-popover.md) with support for single date selection, date range selection, min/max constraints, and custom display formatting. - [Basic Usage](#basic-usage) - [Constructor](#constructor) - [Props](#props) - [Types](#types) - [Date Range Selection](#date-range-selection) - [Min/Max Constraints](#minmax-constraints) - [Custom Display Format](#custom-display-format) - [Event Handling](#event-handling) - [State Variants](#state-variants) - [Styling Examples](#styling-examples) - [Calendar Internals](#calendar-internals) - [All Supported Classes](#all-supported-classes) - [Customizing Theme](#customizing-theme) - [Related Documentation](#related-documentation) ```dart WDatePicker( value: _selectedDate, onChanged: (date) => setState(() => _selectedDate = date), className: 'w-full p-3 border border-gray-300 rounded-lg', placeholder: 'Select a date', ) ``` ## Basic Usage `WDatePicker` renders a trigger container that opens a popover-based calendar on click. It handles month navigation, today highlighting, and date constraints out of the box. ```dart DateTime? _selectedDate; WDatePicker( value: _selectedDate, onChanged: (date) { setState(() => _selectedDate = date); }, className: 'bg-white border rounded-md px-4 py-2 hover:border-blue-500', ) ``` When no `className` is provided, the trigger uses this default styling: ```dart // Default trigger className 'bg-white border border-gray-300 rounded-lg p-3 dark:bg-gray-800 dark:border-gray-600' ``` The calendar popover opens below the trigger and auto-flips if there isn't enough space. In single mode, the popover closes automatically after a date is selected. ## Constructor ```dart const WDatePicker({ Key? key, DatePickerMode mode = DatePickerMode.single, DateTime? value, DateRange? range, ValueChanged? onChanged, ValueChanged? onRangeChanged, DateTime? minDate, DateTime? maxDate, String? className, String placeholder = 'Select date', bool disabled = false, Set states = const {}, DateDisplayFormat? displayFormat, }) ``` ## Props | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `mode` | `DatePickerMode` | `single` | Selection mode: `single` or `range` | | `value` | `DateTime?` | `null` | Currently selected date (single mode) | | `range` | `DateRange?` | `null` | Currently selected range (range mode) | | `onChanged` | `ValueChanged?` | `null` | Callback fired on date selection (single mode) | | `onRangeChanged` | `ValueChanged?` | `null` | Callback fired on range selection (range mode) | | `minDate` | `DateTime?` | `null` | Earliest selectable date | | `maxDate` | `DateTime?` | `null` | Latest selectable date | | `className` | `String?` | `null` | Wind utility classes for the trigger container | | `placeholder` | `String` | `'Select date'` | Text shown when no value is selected | | `disabled` | `bool` | `false` | Prevents interaction, shows forbidden cursor | | `states` | `Set` | `const {}` | Custom states for dynamic styling | | `displayFormat` | `DateDisplayFormat?` | `null` | Custom function to format dates for display | ## Types ### DatePickerMode Determines if the picker operates in single date or date range selection mode. ```dart enum DatePickerMode { single, // Pick a single date range, // Pick a start and end date } ``` ### DateRange Represents a date range with a required start and optional end date. ```dart class DateRange { final DateTime start; final DateTime? end; bool get isComplete => end != null; DateRange copyWith({DateTime? start, DateTime? end}); } ``` ### DateDisplayFormat A typedef for custom date formatting: ```dart typedef DateDisplayFormat = String Function(DateTime date); ``` When no `displayFormat` is provided, dates display as `"Jan 15, 2025"` format. ## Date Range Selection Setting `mode: DatePickerMode.range` enables two-click range selection with hover preview. ```dart DateRange? _dateRange; WDatePicker( mode: DatePickerMode.range, range: _dateRange, onRangeChanged: (range) => setState(() => _dateRange = range), placeholder: 'Check-in / Check-out', className: 'w-64 border p-3 rounded-lg', ) ``` How range selection works: 1. **First click** — Sets the range start. The `onRangeChanged` callback fires with a `DateRange` where `end` is `null`. 2. **Hover** — As the user moves the mouse, dates between start and the hovered date are highlighted with a blue tint. 3. **Second click** — Completes the range. If the second date is before the first, they're automatically swapped. The popover closes. The trigger display text updates throughout: `"Jan 15, 2025 - ..."` while in progress, then `"Jan 15, 2025 - Jan 20, 2025"` when complete. ## Min/Max Constraints Use `minDate` and `maxDate` to restrict which dates are selectable. Dates outside the range appear dimmed and don't respond to clicks. ```dart WDatePicker( value: _selectedDate, onChanged: (date) => setState(() => _selectedDate = date), minDate: DateTime.now(), maxDate: DateTime.now().add(const Duration(days: 90)), className: 'p-3 border rounded-lg', placeholder: 'Next 90 days only', ) ``` > [!NOTE] > Constraints are compared at day-level granularity. Time components are stripped before comparison. ## Custom Display Format Override the default `"Jan 15, 2025"` format with a custom function: ```dart WDatePicker( value: _selectedDate, onChanged: (date) => setState(() => _selectedDate = date), displayFormat: (date) => '${date.day}/${date.month}/${date.year}', className: 'p-3 border rounded-lg', ) ``` In range mode, the format function applies to both start and end dates independently. ## Event Handling ### Single Mode `onChanged` fires with a normalized `DateTime` (midnight, no time component) when the user selects a date: ```dart WDatePicker( value: _date, onChanged: (date) { setState(() => _date = date); _loadSchedule(date); }, ) ``` ### Range Mode `onRangeChanged` fires twice during a range selection — once on the first click (start only) and once on the second click (complete range): ```dart WDatePicker( mode: DatePickerMode.range, range: _range, onRangeChanged: (range) { setState(() => _range = range); if (range.isComplete) { _calculateDuration(range.start, range.end!); } }, ) ``` ## State Variants `WDatePicker` automatically manages several interactive states. Use state prefixes in `className` to apply conditional styles. | State | Activated When | |:------|:---------------| | `hover:` | Mouse hovers over the trigger | | `focus:` | Calendar popover is open | | `open:` | Calendar popover is open (alias for `focus:`) | | `disabled:` | `disabled` prop is `true` | | `selected:` | A date or range has been selected | ```dart WDatePicker( value: _date, onChanged: (date) => setState(() => _date = date), className: 'p-3 border border-gray-300 rounded-lg ' 'hover:border-blue-400 ' 'focus:border-blue-500 focus:ring-2 focus:ring-blue-200 ' 'selected:bg-blue-50 ' 'disabled:opacity-50 disabled:bg-gray-100', ) ``` ### Disabled State When `disabled: true`, the trigger shows a forbidden cursor and the popover won't open: ```dart WDatePicker( disabled: true, value: DateTime(2025, 6, 15), className: 'p-3 border rounded-lg disabled:opacity-50 disabled:bg-gray-100', ) ``` ## Styling Examples ### Default Styling Without a `className`, the trigger uses built-in defaults with dark mode support: ```dart WDatePicker( value: _date, onChanged: (date) => setState(() => _date = date), // Uses: 'bg-white border border-gray-300 rounded-lg p-3 // dark:bg-gray-800 dark:border-gray-600' ) ``` ### Interactive with Ring Focus ```dart WDatePicker( value: _date, onChanged: (date) => setState(() => _date = date), className: 'w-full p-3 bg-white dark:bg-gray-800 ' 'border border-gray-300 dark:border-gray-600 rounded-lg ' 'hover:border-blue-400 dark:hover:border-blue-500 ' 'focus:border-blue-500 focus:ring-2 focus:ring-blue-200 ' 'dark:focus:ring-blue-800 ' 'selected:border-blue-500', ) ``` ### Compact Inline ```dart WDatePicker( value: _date, onChanged: (date) => setState(() => _date = date), className: 'px-2 py-1 text-sm border rounded bg-gray-50 hover:bg-white', placeholder: 'Date', ) ``` ### Borderless with Shadow ```dart WDatePicker( value: _date, onChanged: (date) => setState(() => _date = date), className: 'p-3 bg-white rounded-xl shadow-md hover:shadow-lg', ) ``` ## Calendar Internals The calendar popover is styled with a fixed-width container: ``` 'w-[320px] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-xl p-4' ``` The calendar grid consists of: | Component | Details | |:----------|:--------| | **Header** | Month/year label with left/right navigation arrows | | **Weekday row** | `Mo Tu We Th Fr Sa Su` (Monday start) | | **Date grid** | 6 rows × 7 columns = 42 cells | | **Today** | Highlighted with `bg-gray-100 dark:bg-gray-700 rounded-full` | | **Selected** | `bg-blue-500 text-white rounded-full` | | **In range** | `bg-blue-100 dark:bg-blue-900/30 text-blue-700` | | **Out of month** | `text-gray-300 dark:text-gray-600` | | **Disabled** | `text-gray-300 dark:text-gray-600`, no click | > [!NOTE] > The calendar chrome (header, grid, day cells) uses hardcoded Wind classes and is not configurable via `className`. The `className` prop only controls the trigger element. ## All Supported Classes ### Trigger (className) The `className` prop styles the trigger container. All Wind utility classes are supported: | Category | Examples | |:---------|:---------| | Background | `bg-white`, `bg-gray-50`, `dark:bg-gray-800` | | Border | `border`, `border-2`, `border-gray-300`, `rounded-lg`, `rounded-xl` | | Padding | `p-3`, `px-4`, `py-2` | | Sizing | `w-full`, `w-64`, `w-[300px]` | | Ring | `ring-2`, `ring-blue-200`, `ring-offset-2` | | Shadow | `shadow-sm`, `shadow-md`, `shadow-lg` | | Typography | `text-sm` (affects placeholder/display text indirectly via icon color) | | Opacity | `opacity-50`, `opacity-75` | | State prefixes | `hover:`, `focus:`, `open:`, `disabled:`, `selected:`, `dark:` | | Responsive | `sm:`, `md:`, `lg:`, `xl:`, `2xl:` | ### What className Does NOT Control The calendar popover, header, weekday labels, and day cells use internal Wind classes that are not configurable via props. ## Customizing Theme The calendar's selection colors (`bg-blue-500`, `bg-blue-100`) and neutral grays use Tailwind color scales from `WindThemeData`. Override them to change the visual appearance: ```dart WindTheme( data: WindThemeData( colors: { ...WindThemeData.defaultColors, 'blue': { 100: Color(0xFFDBEAFE), // Range fill 500: Color(0xFF3B82F6), // Selected day 700: Color(0xFF1D4ED8), // Range text 900: Color(0xFF1E3A8A), // Dark mode range }, }, ), child: MyApp(), ) ``` ## Related Documentation - [WFormDatePicker](./w-form-date-picker.md) - Form-integrated date picker with validation - [WSelect](./w-select.md) - Dropdown selection component - [WPopover](./w-popover.md) - The underlying overlay engine - [WInput](./w-input.md) - Standard text input widget