Skip to main content

herogpui_components/date_picker/
picker.rs

1//! DatePicker.
2
3use super::*;
4
5/// The complete state passed to [`DatePicker::content`].
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7#[non_exhaustive]
8pub struct DatePickerRenderState {
9    /// The picker cannot receive focus, edit its field, or open its calendar.
10    pub is_disabled: bool,
11    /// Controlled or constraint validation currently fails.
12    pub is_invalid: bool,
13    /// The field can be focused but not edited or opened.
14    pub is_read_only: bool,
15    /// The picker must contain a date before native form submission.
16    pub is_required: bool,
17    /// A composed child currently owns focus.
18    pub is_focus_within: bool,
19    /// Focus within the picker was reached through keyboard navigation.
20    pub is_focus_visible: bool,
21    /// The calendar popover is currently open.
22    pub is_open: bool,
23}
24
25/// HeroUI DatePicker (controlled open state; selection lives in the entity).
26#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
27#[derive(IntoElement)]
28pub struct DatePicker {
29    /// The locale whose calendar system the popover's grid is drawn in, when
30    /// the caller names one. Forwarded to the embedded [`Calendar`].
31    locale: Option<SharedString>,
32    /// v3's children-as-a-function root composition.
33    content: Option<std::sync::Arc<dyn Fn(DatePickerRenderState) -> gpui::AnyElement + 'static>>,
34    /// `name` — read back by [`DatePicker::form_field`].
35    name: Option<SharedString>,
36    /// `defaultValue` — seeds the state on the first render only.
37    default_value: Option<Date>,
38    /// `value` — v3's controlled date, stored for the first render only. The
39    /// outer `Option` distinguishes an unset builder from `value(null)`, the
40    /// explicitly controlled empty date.
41    value: Option<Option<Date>>,
42    constraints: DateConstraints,
43    is_disabled: bool,
44    is_read_only: bool,
45    is_required: bool,
46    validation_behavior: crate::form::ValidationBehavior,
47    validate: Option<crate::validation::Validator<Option<Date>>>,
48    validation_errors: Vec<SharedString>,
49    is_invalid: bool,
50    auto_focus: bool,
51    on_open_change: Option<std::sync::Arc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
52    state: Entity<CalendarState>,
53    /// `isOpen` — `None` leaves the picker holding the flag, seeded from
54    /// `defaultOpen`.
55    is_open: Option<bool>,
56    default_open: bool,
57    should_close_on_select: bool,
58    /// Popover placement, matching `DatePicker.Popover`'s placement prop.
59    placement: herogpui_core::Placement,
60    label: Option<SharedString>,
61    trigger_indicator: Option<gpui::AnyElement>,
62    on_change: Option<OnChange>,
63    form_state: Rc<RefCell<crate::form::LiveFormFieldState>>,
64    form_is_disabled: Rc<Cell<bool>>,
65    form_default: Rc<RefCell<Option<Date>>>,
66    form_on_change: Rc<RefCell<Option<OnChange>>>,
67    form_field_state: Rc<RefCell<Option<Entity<crate::input::InputState>>>>,
68    /// The `sx` slot, refined over the root style at the end of render.
69    sx: Option<Box<gpui::StyleRefinement>>,
70}
71
72impl DatePicker {
73    /// The locale whose calendar system the popover's grid is drawn in.
74    ///
75    /// Forwarded to the embedded calendar; see [`crate::Calendar::locale`] for
76    /// why this is a builder rather than v3's `I18nProvider`.
77    pub fn locale(mut self, tag: impl Into<SharedString>) -> Self {
78        self.locale = Some(tag.into());
79        self
80    }
81
82    /// `value` — v3's controlled-date spelling, as a pure builder.
83    ///
84    /// The bound [`CalendarState`] owns the selection once the picker
85    /// renders, so this seeds the state on the first render only, winning
86    /// over [`DatePicker::default_value`] the way v3's controlled prop
87    /// outranks the uncontrolled seed; calling `.value(..)` twice keeps the
88    /// last call, like every other builder here. `None` is v3's `null` — an
89    /// explicitly controlled empty date, which still outranks
90    /// `default_value`. A later date is an imperative update rather than a
91    /// builder: write the caller-owned state entity.
92    pub fn value(mut self, date: Option<Date>) -> Self {
93        self.value = Some(date);
94        self
95    }
96
97    /// `minValue`
98    pub fn min_value(mut self, date: Date) -> Self {
99        self.constraints.min_value = Some(date);
100        self
101    }
102
103    /// `maxValue`
104    pub fn max_value(mut self, date: Date) -> Self {
105        self.constraints.max_value = Some(date);
106        self
107    }
108
109    /// `isDateUnavailable`
110    pub fn is_date_unavailable(mut self, f: impl Fn(Date) -> bool + 'static) -> Self {
111        self.constraints.is_date_unavailable = Some(std::sync::Arc::new(f));
112        self
113    }
114
115    /// `firstDayOfWeek`
116    pub fn first_day_of_week(mut self, day: Weekday) -> Self {
117        self.constraints.first_day_of_week = day;
118        self
119    }
120
121    /// All the date constraints at once.
122    pub fn constraints(mut self, constraints: DateConstraints) -> Self {
123        self.constraints = constraints;
124        self
125    }
126
127    /// Sets whether the picker is disabled (`isDisabled`).
128    pub fn is_disabled(mut self, v: bool) -> Self {
129        self.is_disabled = v;
130        self
131    }
132
133    /// `isReadOnly` — keeps the field focusable but blocks edits, selection and opening.
134    pub fn is_read_only(mut self, v: bool) -> Self {
135        self.is_read_only = v;
136        self
137    }
138
139    /// `isRequired` — blocks native form submission while the picker is empty.
140    pub fn is_required(mut self, v: bool) -> Self {
141        self.is_required = v;
142        self
143    }
144
145    /// `validationBehavior` — native errors block submission; ARIA-style errors do not.
146    pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
147        self.validation_behavior = behavior;
148        self
149    }
150
151    /// `validate` — returns one custom message for the selected date.
152    pub fn validate(mut self, f: impl Fn(&Option<Date>) -> Option<SharedString> + 'static) -> Self {
153        self.validate = Some(std::sync::Arc::new(f));
154        self
155    }
156
157    /// `validationErrors` — server messages take precedence over custom validation.
158    pub fn validation_errors(
159        mut self,
160        errors: impl IntoIterator<Item = impl Into<SharedString>>,
161    ) -> Self {
162        self.validation_errors = errors.into_iter().map(Into::into).collect();
163        self
164    }
165
166    /// Sets whether the picker is invalid (`isInvalid`).
167    pub fn is_invalid(mut self, v: bool) -> Self {
168        self.is_invalid = v;
169        self
170    }
171
172    /// `autoFocus` — focuses the editable date field on its first render.
173    pub fn auto_focus(mut self, v: bool) -> Self {
174        self.auto_focus = v;
175        self
176    }
177
178    /// `onOpenChange`
179    pub fn on_open_change(mut self, f: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
180        self.on_open_change = Some(std::sync::Arc::new(f));
181        self
182    }
183
184    /// Creates a date picker bound to `state`.
185    pub fn new(state: Entity<CalendarState>) -> Self {
186        let form_state = date_picker_form_state(state.entity_id().as_u64());
187        let form_is_disabled = Rc::new(Cell::new(false));
188        let form_default = Rc::new(RefCell::new(None));
189        let form_on_change: Rc<RefCell<Option<OnChange>>> = Rc::new(RefCell::new(None));
190        let form_field_state = Rc::new(RefCell::new(None::<Entity<crate::input::InputState>>));
191        let restore_form_state = Rc::downgrade(&form_state);
192        let restore_is_disabled = form_is_disabled.clone();
193        let restore_default = form_default.clone();
194        let restore_callback = form_on_change.clone();
195        let restore_state = state.clone();
196        let restore_field_state = form_field_state.clone();
197        // FormField's restore slot is an Arc, but this callback is intentionally
198        // confined to the single-threaded GPUI app and captures its live state.
199        #[allow(clippy::arc_with_non_send_sync)]
200        let restore: std::sync::Arc<dyn Fn(&mut Window, &mut App)> =
201            std::sync::Arc::new(move |window, cx| {
202                let date = *restore_default.borrow();
203                restore_state.update(&mut *cx, |state, cx| {
204                    state.selected = date;
205                    state.selected_dates = date.into_iter().collect();
206                    if let Some(date) = date {
207                        state.view_year = date.year;
208                        state.view_month = date.month;
209                        state.view_day = date.day;
210                    }
211                    cx.notify();
212                });
213                if let Some(field_state) = restore_field_state.borrow().clone() {
214                    let text = date.map(|date| date.format_iso()).unwrap_or_default();
215                    field_state.update(cx, |state, cx| {
216                        state.set_value(text);
217                        cx.notify();
218                    });
219                }
220                if let Some(state) = restore_form_state.upgrade() {
221                    let mut state = state.borrow_mut();
222                    state.value = crate::form::FormValue::Text(
223                        date.map(|date| date.format_iso())
224                            .unwrap_or_default()
225                            .into(),
226                    );
227                    state.is_invalid = false;
228                    state.is_successful = !restore_is_disabled.get();
229                }
230                if let Some(callback) = restore_callback.borrow().as_ref() {
231                    callback(&date, window, cx);
232                }
233            });
234        form_state.borrow_mut().restore = Some(restore);
235        Self {
236            locale: None,
237            content: None,
238            name: None,
239            default_value: None,
240            value: None,
241            constraints: DateConstraints::new(),
242            is_disabled: false,
243            is_read_only: false,
244            is_required: false,
245            validation_behavior: crate::form::ValidationBehavior::Native,
246            validate: None,
247            validation_errors: Vec::new(),
248            is_invalid: false,
249            auto_focus: false,
250            on_open_change: None,
251            state,
252            is_open: None,
253            default_open: false,
254            should_close_on_select: true,
255            placement: herogpui_core::Placement::Bottom,
256            label: None,
257            trigger_indicator: None,
258            on_change: None,
259            form_state,
260            form_is_disabled,
261            form_default,
262            form_on_change,
263            form_field_state,
264            sx: None,
265        }
266    }
267
268    /// `name` — the name this picker submits under.
269    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
270        self.name = Some(name.into());
271        self
272    }
273
274    /// The `Form` field this picker submits, when it has a `name`.
275    ///
276    /// The date is written ISO-8601, which is what an HTML `<input type="date">`
277    /// submits. Needs `cx` because the selection lives in the state entity.
278    pub fn form_field(&self, cx: &App) -> Option<crate::form::FormField> {
279        let name = self.name.clone()?;
280        self.form_is_disabled.set(self.is_disabled);
281        let selected = self.state.read(cx).selected();
282        let text = selected.map(|d| d.format_iso()).unwrap_or_default();
283        if self.form_default.borrow().is_none() {
284            *self.form_default.borrow_mut() = self.state.read(cx).selected;
285        }
286        let mut form_state = self.form_state.borrow_mut();
287        form_state.value = crate::form::FormValue::Text(text.into());
288        form_state.is_successful = !self.is_disabled;
289        let constraint_error = selected
290            .is_some_and(|date| !self.constraints.allows(date))
291            .then(|| SharedString::from("That date is unavailable."));
292        form_state.is_invalid = crate::validation::resolve(
293            self.is_invalid,
294            &self.validation_errors,
295            self.validate
296                .as_ref()
297                .and_then(|validate| validate(&selected)),
298            constraint_error,
299        )
300        .is_invalid;
301        Some(
302            crate::form::FormField::live(name, self.form_state.clone())
303                .is_required(self.is_required)
304                .validation_behavior(self.validation_behavior),
305        )
306    }
307
308    /// `defaultValue` — the uncontrolled initial selection.
309    ///
310    /// Written into the state on the first render only, so it seeds the
311    /// component without fighting the user afterwards.
312    pub fn default_value(mut self, value: Date) -> Self {
313        self.default_value = Some(value);
314        *self.form_default.borrow_mut() = Some(value);
315        self
316    }
317
318    /// Sets the controlled open state of the popover (`isOpen`).
319    pub fn is_open(mut self, v: bool) -> Self {
320        self.is_open = Some(v);
321        self
322    }
323    /// `defaultOpen` — the uncontrolled initial popover state.
324    ///
325    /// Only consulted when `is_open` is not supplied; the picker then owns the
326    /// flag and its trigger toggles it.
327    pub fn default_open(mut self, v: bool) -> Self {
328        self.default_open = v;
329        self
330    }
331
332    /// `shouldCloseOnSelect` — whether a calendar pick dismisses the popover.
333    pub fn should_close_on_select(mut self, v: bool) -> Self {
334        self.should_close_on_select = v;
335        self
336    }
337
338    /// `DatePicker.Popover.placement` — positions the calendar relative to
339    /// the trigger. The default is HeroUI's `bottom`; start/end and side
340    /// placements use the shared GPUI placement vocabulary.
341    pub fn placement(mut self, placement: herogpui_core::Placement) -> Self {
342        self.placement = placement;
343        self
344    }
345
346    /// Sets the label shown above the picker.
347    pub fn label(mut self, l: impl Into<SharedString>) -> Self {
348        self.label = Some(l.into());
349        self
350    }
351
352    /// `DatePicker.TriggerIndicator` — replaces the default calendar glyph.
353    pub fn trigger_indicator(mut self, indicator: impl IntoElement) -> Self {
354        self.trigger_indicator = Some(indicator.into_any_element());
355        self
356    }
357
358    /// v3's `children` render function, handed the complete resolved root state.
359    pub fn content(
360        mut self,
361        render: impl Fn(DatePickerRenderState) -> gpui::AnyElement + 'static,
362    ) -> Self {
363        self.content = Some(std::sync::Arc::new(render));
364        self
365    }
366
367    /// Sets the handler called when the date changes (`onChange`).
368    pub fn on_change(mut self, f: impl Fn(&Option<Date>, &mut Window, &mut App) + 'static) -> Self {
369        let callback = std::sync::Arc::new(f);
370        *self.form_on_change.borrow_mut() = Some(callback.clone());
371        self.on_change = Some(callback);
372        self
373    }
374
375    /// The one slot for caller-owned low-level styling: GPUI's styling methods
376    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
377    /// applied to the picker's root element after every value the component
378    /// and the active theme chose, so they win.
379    pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
380        crate::util::refine_sx(&mut self.sx, style);
381        self
382    }
383}
384
385impl RenderOnce for DatePicker {
386    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
387        self.form_is_disabled.set(self.is_disabled);
388        // Every keyed slot and focus handle below hangs off this one base, so
389        // no two of them can flatten into a shared key.
390        let base_id = gpui::ElementId::named_usize("dp", self.state.entity_id().as_u64() as usize);
391        // `value` / `defaultValue` seed the state once, before anything reads
392        // it. `value` is v3's controlled spelling, so it outranks the
393        // uncontrolled seed; the state owns the selection afterwards, and a
394        // write to the caller-owned entity is the imperative update.
395        if let Some(date) = self.value {
396            let state = self.state.clone();
397            crate::util::seed_once(
398                window,
399                cx,
400                gpui::ElementId::named_usize(
401                    "datepicker-default",
402                    self.state.entity_id().as_u64() as usize,
403                ),
404                move |cx| {
405                    state.update(cx, |s, cx| {
406                        s.selected = date;
407                        s.selected_dates = date.into_iter().collect();
408                        if let Some(date) = date {
409                            s.view_year = date.year;
410                            s.view_month = date.month;
411                            s.view_day = date.day;
412                        }
413                        cx.notify();
414                    });
415                },
416            );
417        } else if let Some(value) = self.default_value {
418            let state = self.state.clone();
419            crate::util::seed_once(
420                window,
421                cx,
422                gpui::ElementId::named_usize(
423                    "datepicker-default",
424                    self.state.entity_id().as_u64() as usize,
425                ),
426                move |cx| {
427                    state.update(cx, |s, cx| {
428                        s.selected = Some(value);
429                        s.selected_dates = vec![value];
430                        s.view_year = value.year;
431                        s.view_month = value.month;
432                        s.view_day = value.day;
433                        cx.notify();
434                    });
435                },
436            );
437        }
438
439        // `controlled` takes `cx` mutably, so it precedes the theme tokens.
440        let (is_open, open_own) = crate::util::controlled(
441            window,
442            cx,
443            element_id::scoped(&base_id, "open"),
444            self.is_open,
445            self.default_open,
446        );
447        let open = is_open && !self.is_disabled;
448        let selected = self.state.read(cx).selected;
449        let selected_text = selected.map(|date| date.format_iso()).unwrap_or_default();
450        self.form_state.borrow_mut().value =
451            crate::form::FormValue::Text(selected_text.clone().into());
452        let initial_text = selected_text.clone();
453        let field_state = window
454            .use_keyed_state(
455                element_id::scoped(&base_id, "field-state"),
456                cx,
457                move |_, cx| cx.new(|cx| crate::input::InputState::with_value(cx, initial_text)),
458            )
459            .read(cx)
460            .clone();
461        *self.form_field_state.borrow_mut() = Some(field_state.clone());
462        let field_sync = window.use_keyed_state(element_id::scoped(&base_id, "field-sync"), cx, {
463            let selected_text = selected_text.clone();
464            move |_, _| selected_text
465        });
466        let field_value = field_state.read(cx).value().to_owned();
467        let last_field_value = field_sync.read(cx).clone();
468        let field_follows_selection =
469            field_value == last_field_value && field_value != selected_text;
470        if field_follows_selection {
471            field_state.update(cx, |state, _| state.set_value(selected_text.clone()));
472        }
473        let live_field_value = if field_follows_selection {
474            selected_text.clone()
475        } else {
476            field_value
477        };
478        if live_field_value == selected_text {
479            field_sync.update(cx, |value, _| *value = selected_text.clone());
480        }
481        let (parsed, constraint_error) = if live_field_value.trim().is_empty() {
482            (None, None)
483        } else {
484            let (parsed, _) = parse_value(&live_field_value);
485            let error = match parsed {
486                None => Some(SharedString::from("Enter a valid date.")),
487                Some(date) if !self.constraints.allows(date) => {
488                    Some(SharedString::from("That date is unavailable."))
489                }
490                Some(_) => None,
491            };
492            (parsed, error)
493        };
494        let field_invalid = crate::validation::resolve(
495            self.is_invalid,
496            &self.validation_errors,
497            self.validate
498                .as_ref()
499                .and_then(|validate| validate(&parsed)),
500            constraint_error,
501        )
502        .is_invalid;
503        {
504            let mut state = self.form_state.borrow_mut();
505            state.value = crate::form::FormValue::Text(live_field_value.into());
506            state.is_successful = !self.is_disabled;
507            state.is_invalid = field_invalid;
508        }
509        let field_focus = field_state.read(cx).focus_handle(cx);
510        self.form_state.borrow_mut().focus = Some(field_focus.clone());
511        if let Some(render) = self.content.clone() {
512            let content_scope = window
513                .use_keyed_state(
514                    element_id::scoped(&base_id, "content-focus"),
515                    cx,
516                    |_, cx| cx.focus_handle().tab_stop(false),
517                )
518                .read(cx)
519                .clone();
520            let is_focus_within = content_scope.contains_focused(window, cx);
521            let content_root = gpui::div()
522                .relative()
523                .max_w(px(320.))
524                .track_focus(&content_scope)
525                .child(render(DatePickerRenderState {
526                    is_disabled: self.is_disabled,
527                    is_invalid: field_invalid,
528                    is_read_only: self.is_read_only,
529                    is_required: self.is_required,
530                    is_focus_within,
531                    is_focus_visible: is_focus_within && crate::util::focus_visible(cx),
532                    is_open: open,
533                }));
534            return crate::util::apply_sx(content_root, &self.sx).into_any_element();
535        }
536        let (overlay_phase, dismissal_token) = crate::util::overlay_scope(
537            window,
538            cx,
539            element_id::scoped(&base_id, "overlay"),
540            open,
541            // HeroUI keeps the popover alive for its 100ms fade/zoom exit.
542            // The calendar callback below is gated by `panel_open` so a
543            // retained cell cannot receive the same press again.
544            true,
545        );
546        let panel_visible = overlay_phase != crate::util::OverlayPhase::Closed;
547        let panel_open = overlay_phase == crate::util::OverlayPhase::Open;
548        // The positioner discovers a viewport flip during prepaint. Keep the
549        // requested and resolved sides in keyed state so the next render's
550        // entry animation follows the physical side that is actually painted.
551        let requested_placement = window.use_keyed_state(
552            element_id::scoped(&base_id, "requested-placement"),
553            cx,
554            |_, _| self.placement,
555        );
556        let resolved_placement = window.use_keyed_state(
557            element_id::scoped(&base_id, "resolved-placement"),
558            cx,
559            |_, _| Rc::new(Cell::new(None::<crate::popover::PopoverPlacement>)),
560        );
561        if *requested_placement.read(cx) != self.placement {
562            requested_placement.update(cx, |placement, _| *placement = self.placement);
563            resolved_placement.read(cx).set(None);
564        }
565        let resolved_placement = resolved_placement.read(cx).clone();
566        let entry_placement = resolved_placement.get().unwrap_or(self.placement);
567        let blur_open_own = open_own.clone();
568        let blur_open_change = self.on_open_change.clone();
569        let blur_scope =
570            crate::util::close_on_blur(window, cx, &base_id, open, move |window, cx| {
571                if let Some(held) = &blur_open_own {
572                    held.update(cx, |value, cx| {
573                        *value = false;
574                        cx.notify();
575                    });
576                }
577                if let Some(callback) = &blur_open_change {
578                    callback(&false, window, cx);
579                }
580            });
581        let trigger_focus =
582            crate::util::tab_stop_handle(element_id::scoped(&base_id, "trigger-focus"), window, cx);
583        let initiator =
584            window.use_keyed_state(element_id::scoped(&base_id, "initiator"), cx, |_, _| 0usize);
585        let trigger_pressed = Rc::new(Cell::new(false));
586
587        let open_own_keys = open_own.clone();
588        let open_cb_keys = self.on_open_change.clone();
589        let open_picker = crate::util::shared(move |window: &mut Window, cx: &mut App| {
590            if is_open {
591                return;
592            }
593            if let Some(held) = &open_own_keys {
594                held.update(cx, |open, cx| {
595                    *open = true;
596                    cx.notify();
597                });
598            }
599            if let Some(cb) = &open_cb_keys {
600                cb(&true, window, cx);
601            }
602        });
603
604        let selected_state = self.state.clone();
605        let user_change = self.on_change.clone();
606        let form_state = self.form_state.clone();
607        let field_forced_invalid = self.is_invalid;
608        let was_open = is_open;
609        let trigger_pressed_for_capture = trigger_pressed.clone();
610        let trigger_open_own = open_own.clone();
611        let trigger_open_cb = self.on_open_change.clone();
612        let trigger_indicator = self.trigger_indicator.unwrap_or_else(|| {
613            gpui::svg()
614                .size(px(16.))
615                .path(icons::CALENDAR)
616                .text_color(cx.colors().muted)
617                .into_any_element()
618        });
619        let mut trigger = gpui::div()
620            .id(element_id::scoped(&base_id, "trigger"))
621            .flex()
622            .items_center()
623            .justify_center()
624            .size(px(24.))
625            .child(
626                gpui::div()
627                    // `.date-picker__trigger-indicator` is `size-4` and centers
628                    // either the default glyph or caller-supplied content.
629                    .flex()
630                    .items_center()
631                    .justify_center()
632                    .size(px(16.))
633                    .child(trigger_indicator),
634            );
635        // The trigger is a bare 24px square with no `rounded()` of its own, so
636        // the overlay ring is a plain square band at radius zero.
637        trigger = crate::util::ring_overlay_if_focused(
638            trigger,
639            &trigger_focus,
640            true,
641            px(0.),
642            Vec::new(),
643            window,
644            cx,
645        );
646        // `useDatePicker` derives the trigger from `useOverlayTrigger`, so
647        // it is a button with `aria-expanded`. `aria-haspopup` has no gpui
648        // builder (see `crate::a11y`).
649        trigger = trigger
650            .a11y_named(
651                a11y::Role::Button,
652                &a11y::Name::labelled(crate::i18n::ui_string(crate::i18n::UiString::Calendar, cx)),
653            )
654            .a11y_expanded(is_open);
655        if !self.is_disabled && !self.is_read_only {
656            let focus_on_press = trigger_focus.clone();
657            let trigger_initiator = initiator.clone();
658            trigger = trigger
659                .capture_any_mouse_down(move |_, _, cx| {
660                    trigger_pressed_for_capture.set(true);
661                    let pressed = trigger_pressed_for_capture.clone();
662                    cx.defer(move |_| pressed.set(false));
663                })
664                .track_focus(&trigger_focus)
665                .cursor(crate::util::interactive_cursor(cx))
666                .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
667                    window.focus(&focus_on_press, cx);
668                    cx.stop_propagation();
669                })
670                .on_click(move |_, window, cx| {
671                    trigger_initiator.update(cx, |part, _| *part = 1);
672                    if let Some(held) = &trigger_open_own {
673                        held.update(cx, |open, cx| {
674                            *open = !was_open;
675                            cx.notify();
676                        });
677                    }
678                    if let Some(cb) = &trigger_open_cb {
679                        cb(&!was_open, window, cx);
680                    }
681                });
682        }
683        if !self.is_disabled && !self.is_read_only {
684            trigger = crate::util::record_focus_bounds(trigger, &trigger_focus, window, cx);
685        }
686        let mut date_field = DateField::new(field_state)
687            .embedded(false)
688            .full_width(true)
689            .is_disabled(self.is_disabled)
690            .is_read_only(self.is_read_only)
691            .is_required(self.is_required)
692            .validation_behavior(self.validation_behavior)
693            .is_invalid(field_invalid)
694            .auto_focus(self.auto_focus)
695            .constraints(self.constraints.clone())
696            .report_invalid_changes()
697            .when_some(self.locale.clone(), |field, tag| field.locale(tag))
698            .suffix(trigger);
699        let open_from_field = open_picker.clone();
700        let field_initiator = initiator.clone();
701        let field_constraints = self.constraints.clone();
702        let field_validate = self.validate.clone();
703        let field_validation_errors = self.validation_errors.clone();
704        date_field = date_field
705            .on_picker_open(move |window, cx| {
706                field_initiator.update(cx, |part, _| *part = 0);
707                open_from_field(window, cx);
708            })
709            .on_change(move |date, window, cx| {
710                let valid_date = date.filter(|date| field_constraints.allows(*date));
711                let constraint_error = (date.is_some() && valid_date.is_none())
712                    .then(|| SharedString::from("That date is unavailable."));
713                form_state.borrow_mut().is_invalid = crate::validation::resolve(
714                    field_forced_invalid,
715                    &field_validation_errors,
716                    field_validate
717                        .as_ref()
718                        .and_then(|validate| validate(&valid_date)),
719                    constraint_error,
720                )
721                .is_invalid;
722                selected_state.update(cx, |state, cx| {
723                    if date.is_none() || valid_date.is_some() {
724                        state.selected = valid_date;
725                        state.selected_dates = valid_date.into_iter().collect();
726                    }
727                    if let Some(date) = valid_date {
728                        state.view_year = date.year;
729                        state.view_month = date.month;
730                        state.view_day = date.day;
731                    }
732                    cx.notify();
733                });
734                if date.is_none() || valid_date.is_some() {
735                    form_state.borrow_mut().value = crate::form::FormValue::Text(
736                        valid_date
737                            .map(|date| date.format_iso())
738                            .unwrap_or_default()
739                            .into(),
740                    );
741                }
742                if date.is_none() || valid_date.is_some() {
743                    if let Some(cb) = &user_change {
744                        cb(&valid_date, window, cx);
745                    }
746                }
747            });
748
749        // `useDatePicker` is `role: 'group'` on the field box.
750        let field = gpui::div()
751            .id(base_id.clone())
752            .a11y_named(a11y::Role::Group, &a11y::Name::maybe(self.label.clone()))
753            .w_full()
754            .child(date_field);
755        let anchor_bounds = window
756            .use_keyed_state(element_id::scoped(&base_id, "anchor-bounds"), cx, |_, _| {
757                Rc::new(Cell::new(None::<gpui::Bounds<Pixels>>))
758            })
759            .read(cx)
760            .clone();
761        let field = crate::popover::PopoverTriggerMeasure::new(field, anchor_bounds.clone());
762
763        let mut root = gpui::div().relative().max_w(px(320.));
764        let mut wrapper = gpui::div().flex().flex_col().gap(px(4.)).w_full();
765        if let Some(label) = &self.label {
766            wrapper = wrapper.child(
767                crate::field::Label::new(label.clone())
768                    .is_required(self.is_required)
769                    .is_disabled(self.is_disabled)
770                    .is_invalid(self.is_invalid),
771            );
772        }
773        wrapper = wrapper.child(field);
774        root = root.child(wrapper);
775
776        if panel_visible {
777            // React Aria dismisses the panel on Escape, on a press outside it
778            // and once a day is chosen; all three write the same flag. Escape
779            // rides on the root, not the panel: focusing the panel would take
780            // the arrows away from the calendar grid inside it.
781            let close_own = open_own;
782            let close_cb = self.on_open_change.clone();
783            let restore_field = field_focus;
784            let restore_trigger = trigger_focus;
785            let restore_part = initiator;
786            let close = crate::util::shared(move |window: &mut Window, cx: &mut App| {
787                if let Some(held) = &close_own {
788                    held.update(cx, |v, cx| {
789                        *v = false;
790                        cx.notify();
791                    });
792                }
793                if let Some(cb) = &close_cb {
794                    cb(&false, window, cx);
795                }
796                if *restore_part.read(cx) == 1 {
797                    window.focus(&restore_trigger, cx);
798                } else {
799                    window.focus(&restore_field, cx);
800                }
801            });
802
803            let mut cal = Calendar::new(self.state.clone())
804                .constraints(self.constraints.clone())
805                .when_some(self.locale.clone(), |cal, tag| cal.locale(tag))
806                .is_disabled(self.is_disabled)
807                .is_read_only(self.is_read_only)
808                .inert(!panel_open)
809                // React Aria moves the focus into the calendar as the popover
810                // opens, so the arrows work straight away.
811                .autofocus_grid(panel_open)
812                .is_invalid(field_invalid);
813            // The calendar reports the chosen date; the picker owns the open
814            // flag, so closing belongs here, in the picker's own reaction to
815            // that report, not inside the calendar (a bare `Calendar` has
816            // nothing to close). The pick also fires the caller's `on_change`
817            // first, so both events read the same selection.
818            let pick_close = close.clone();
819            let user_change = self.on_change.clone();
820            let form_state = self.form_state.clone();
821            let calendar_forced_invalid = self.is_invalid;
822            let calendar_validate = self.validate.clone();
823            let calendar_validation_errors = self.validation_errors.clone();
824            let should_close_on_select = self.should_close_on_select;
825            let panel_open_for_change = panel_open;
826            cal = cal.on_change(move |d, window, cx| {
827                if !panel_open_for_change {
828                    return;
829                }
830                let mut form_state = form_state.borrow_mut();
831                form_state.value = crate::form::FormValue::Text(
832                    d.map(|date| date.format_iso()).unwrap_or_default().into(),
833                );
834                form_state.is_invalid = crate::validation::resolve(
835                    calendar_forced_invalid,
836                    &calendar_validation_errors,
837                    calendar_validate.as_ref().and_then(|validate| validate(d)),
838                    None,
839                )
840                .is_invalid;
841                if let Some(cb) = &user_change {
842                    cb(d, window, cx);
843                }
844                if should_close_on_select {
845                    pick_close(window, cx);
846                }
847            });
848            let esc = close.clone();
849            root = crate::util::dismiss_on_escape_with_token(
850                root,
851                dismissal_token.clone(),
852                move |window, cx| {
853                    esc(window, cx);
854                    crate::util::DismissResult::Handled
855                },
856            );
857            let outside_close = close.clone();
858            let panel_open_for_dismissal = panel_open;
859            let panel = crate::util::dismiss_on_press_outside_with_token(
860                picker_panel(
861                    cx,
862                    "date-picker-popover",
863                    element_id::scoped(&base_id, "popover"),
864                )
865                .child(cal),
866                dismissal_token,
867                move |window, cx| {
868                    if !panel_open_for_dismissal {
869                        return crate::util::DismissResult::Declined;
870                    }
871                    if trigger_pressed.get() {
872                        return crate::util::DismissResult::Declined;
873                    }
874                    outside_close(window, cx);
875                    crate::util::DismissResult::Handled
876                },
877            );
878            let (slide_x, slide_y) = placement_entry_offset(entry_placement);
879            let zoom = crate::anim::ZoomBox::panel(px(12.), px(20.)).padding_x(px(12.));
880            let zoom = crate::anim::ZoomBox {
881                slide_x: (slide_x != 0.0).then(|| px(slide_x)),
882                slide_y: (slide_y != 0.0).then(|| px(slide_y)),
883                ..zoom
884            };
885            let panel = if overlay_phase == crate::util::OverlayPhase::Exiting {
886                crate::anim::exiting(
887                    panel,
888                    element_id::scoped(&base_id, "panel-out"),
889                    zoom,
890                    crate::anim::Motion::LIST_OUT,
891                    cx,
892                )
893            } else {
894                crate::anim::entering_zoom(
895                    panel,
896                    element_id::scoped(&base_id, "panel"),
897                    zoom,
898                    crate::anim::Motion::LIST_IN,
899                    cx,
900                )
901            };
902            root = root.child(crate::util::floating(
903                crate::popover::popover_with_resolved_placement(
904                    anchor_bounds,
905                    self.placement,
906                    px(PICKER_POPOVER_OFFSET),
907                    Some(resolved_placement),
908                    panel,
909                ),
910            ));
911        }
912
913        crate::util::apply_sx(root.track_focus(&blur_scope), &self.sx).into_any_element()
914    }
915}
916
917/// The popover chrome every picker shares — `.date-picker__popover` is
918/// `bg-overlay p-3` at `min(32px, calc(--radius * 2.5))` with `--shadow-overlay`.
919///
920/// The calendars used to paint this themselves, which put a second panel inside
921/// the first one and left a standalone `Calendar` looking like a floating card.
922pub(super) fn picker_panel(
923    cx: &App,
924    selector: &'static str,
925    id: gpui::ElementId,
926) -> gpui::Stateful<gpui::Div> {
927    let colors = cx.colors();
928    let layout = cx.layout();
929    gpui::div()
930        .id(id)
931        // `.date-picker__popover` and `.date-range-picker__popover` are `p-3`.
932        .p(px(12.))
933        // The pinned 8px base makes their `min(32px, radius * 2.5)` exactly 20px.
934        .rounded(px(20.))
935        .max_h_full()
936        .overflow_y_scroll()
937        .restrict_scroll_to_axis()
938        .debug_selector(move || selector.to_owned())
939        .bg(colors.overlay.background)
940        .text_color(colors.overlay.foreground)
941        .when(!layout.overlay_shadow.is_empty(), |e| {
942            e.shadow(layout.overlay_shadow.clone())
943        })
944}
945
946// ---------------------------------------------------------------------------
947
948crate::util::impl_component_styled!(DatePicker);