playbook_ui 16.10.0.pre.alpha.play300617418 → 16.10.0.pre.alpha.play300617429

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_date_picker/_date_picker.scss +6 -0
  3. data/app/pb_kits/playbook/pb_date_picker/_date_picker.tsx +16 -2
  4. data/app/pb_kits/playbook/pb_date_picker/date_picker.html.erb +58 -1
  5. data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +310 -64
  6. data/app/pb_kits/playbook/pb_dialog/_dialog.scss +54 -1
  7. data/app/pb_kits/playbook/pb_dialog/_dialog.tsx +21 -5
  8. data/app/pb_kits/playbook/pb_dialog/_dialog_context.tsx +8 -2
  9. data/app/pb_kits/playbook/pb_dialog/child_kits/_dialog_header.tsx +1 -1
  10. data/app/pb_kits/playbook/pb_dialog/dialog.html.erb +5 -1
  11. data/app/pb_kits/playbook/pb_dialog/dialog.test.jsx +125 -0
  12. data/app/pb_kits/playbook/pb_dialog/index.js +11 -1
  13. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +8 -0
  14. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +101 -31
  15. data/app/pb_kits/playbook/pb_dropdown/docs/_playground.json +149 -237
  16. data/app/pb_kits/playbook/pb_dropdown/docs/_playground.overrides.json +58 -74
  17. data/app/pb_kits/playbook/pb_dropdown/index.js +220 -25
  18. data/app/pb_kits/playbook/pb_dropdown/keyboard_accessibility.js +9 -10
  19. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownContainer.tsx +39 -2
  20. data/app/pb_kits/playbook/pb_icon/docs/_playground.json +2 -16
  21. data/app/pb_kits/playbook/pb_icon/docs/_playground.overrides.json +2 -13
  22. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.scss +7 -7
  23. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +125 -10
  24. data/app/pb_kits/playbook/pb_popover/_popover.scss +27 -0
  25. data/app/pb_kits/playbook/pb_popover/_popover.tsx +49 -16
  26. data/app/pb_kits/playbook/pb_popover/index.ts +35 -4
  27. data/app/pb_kits/playbook/pb_popover/popover.html.erb +5 -2
  28. data/app/pb_kits/playbook/pb_popover/popover.test.js +212 -5
  29. data/app/pb_kits/playbook/pb_time_picker/_time_picker.scss +42 -0
  30. data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +309 -167
  31. data/app/pb_kits/playbook/pb_time_picker/index.ts +263 -11
  32. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +109 -3
  33. data/app/pb_kits/playbook/pb_typeahead/kit.schema.json +6 -0
  34. data/app/pb_kits/playbook/utilities/floatingPortalHosts.ts +565 -0
  35. data/dist/chunks/{_pb_line_graph-BQrN-uXU.js → _pb_line_graph-C2H9vs_8.js} +1 -1
  36. data/dist/chunks/_typeahead-qxLestd5.js +5 -0
  37. data/dist/chunks/{globalProps-C04xFof_.js → globalProps-DIAZ03-L.js} +1 -1
  38. data/dist/chunks/{lib-BhWFApsB.js → lib-ZHDedNfP.js} +2 -2
  39. data/dist/chunks/vendor.js +5 -5
  40. data/dist/playbook-rails-react-bindings.js +1 -1
  41. data/dist/playbook-rails.js +1 -1
  42. data/dist/playbook.css +1 -1
  43. data/lib/playbook/version.rb +1 -1
  44. metadata +7 -6
  45. data/dist/chunks/_typeahead-IofDd4w9.js +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eea9e460fe3172005e0ffb859d2110dfd98731a78012d2e0c980fa030d16e51b
4
- data.tar.gz: a9ff3972f4b7c8f841626687a74ea0475a3f6e3ccba811df5f3a6d7a4ecd7f6c
3
+ metadata.gz: cc61e3b1cf418d10b8c89717465ec82d728f918711ab223e03065119d0e6ebe2
4
+ data.tar.gz: a59e2de7ba4dbba8477676d95664dd0442148d2effd74ad5c9f1162e7e451693
5
5
  SHA512:
6
- metadata.gz: 461c23cf434287c4d6b23afdaa85ac7f66737a9b546652ef7508cf95d06c61b2d3b3b05e7db5f5711a37a5663c8fb967cd7959f3fb32c2bd8d2bce5414252cf8
7
- data.tar.gz: a854d92ff8049450212c46c721930ba6dac1350107582e06749b240af20efaf7f6debeccfebd10da39c6b323dff1752b08c7f9154c0625fce38ecb56b377b30e
6
+ metadata.gz: 0dd024e07261c2ead3b178c94561265f0a64caa2f07d177c71f2ef325163243c630d4903290151e7cf3eff0c70c3a1840ba56d646d31c6fff49107c37e1066e9
7
+ data.tar.gz: d16f9553b4b72dca3f94d39b9d3dc29529bf17c0dcb24b0154fe127e610298f9f155d1533df6e2f90a7ef6c86d5078c67b4f8dc102fe6a7a6d97f2053a5d87ac
@@ -80,3 +80,9 @@
80
80
  content: none;
81
81
  }
82
82
  }
83
+
84
+ .pb_date_picker_floating_shell {
85
+ margin: 0;
86
+ padding: 0;
87
+ pointer-events: auto;
88
+ }
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from 'react'
1
+ import React, { useContext, useEffect } from 'react'
2
2
  import classnames from 'classnames'
3
3
 
4
4
  import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props'
@@ -6,6 +6,7 @@ import { deprecatedProps, globalProps, GlobalProps } from '../utilities/globalPr
6
6
  import { getAllIcons } from "../utilities/icons/allicons"
7
7
  import { camelToSnakeCase } from '../utilities/text'
8
8
 
9
+ import { DialogContext } from '../pb_dialog/_dialog_context'
9
10
  import datePickerHelper from './date_picker_helper'
10
11
  import Icon from '../pb_icon/_icon'
11
12
  import Caption from '../pb_caption/_caption'
@@ -149,6 +150,8 @@ const DatePicker = (props: DatePickerProps): React.ReactElement => {
149
150
  syncEndWith,
150
151
  } = props
151
152
 
153
+ const dialogCtx = useContext(DialogContext)
154
+
152
155
  const ariaProps = buildAriaProps(aria)
153
156
  const normalizedDefaultDate = normalizeDefaultDate(defaultDate)
154
157
  const filterResetDefaultSerialized = serializeDefaultDateForFilterReset(normalizedDefaultDate)
@@ -186,6 +189,7 @@ const DatePicker = (props: DatePickerProps): React.ReactElement => {
186
189
  format,
187
190
  hideIcon,
188
191
  inLine,
192
+ inline: inLine,
189
193
  maxDate,
190
194
  minDate,
191
195
  mode,
@@ -208,8 +212,18 @@ const DatePicker = (props: DatePickerProps): React.ReactElement => {
208
212
  syncStartWith,
209
213
  syncEndWith,
210
214
  required: false,
215
+ dialogPortalTarget: dialogCtx?.selectMenuPortalTarget ?? null,
211
216
  }, scrollContainer)
212
- }, initializeOnce ? [] : undefined)
217
+
218
+ return () => {
219
+ const input = document.getElementById(String(pickerId)) as
220
+ | (HTMLElement & { _flatpickr?: { destroy: () => void }; _pbDatePickerOpenUnsub?: () => void })
221
+ | null
222
+ input?._pbDatePickerOpenUnsub?.()
223
+ delete input?._pbDatePickerOpenUnsub
224
+ input?._flatpickr?.destroy()
225
+ }
226
+ }, initializeOnce ? [initializeOnce, dialogCtx?.selectMenuPortalTarget, pickerId] : undefined)
213
227
 
214
228
  const filteredProps = {...props}
215
229
  if (filteredProps.marginBottom === undefined) {
@@ -96,22 +96,73 @@
96
96
  })
97
97
  }
98
98
 
99
+ const disconnectPopoverWatch = (input) => {
100
+ if (!input) return
101
+
102
+ if (input._pbDatePickerPopoverObserver) {
103
+ input._pbDatePickerPopoverObserver.disconnect()
104
+ delete input._pbDatePickerPopoverObserver
105
+ }
106
+
107
+ delete input.dataset.pbDatePickerPopoverWatch
108
+ }
109
+
99
110
  const loadDatePicker = () => {
100
111
  const input = document.getElementById("<%= object.picker_id %>")
101
112
 
102
- if (!input || input._flatpickr) return true
113
+ if (!input || input._flatpickr) {
114
+ disconnectPopoverWatch(input)
115
+ return true
116
+ }
103
117
  if (typeof window.datePickerHelper !== "function") return false
104
118
 
119
+ const popoverTooltip = input.closest(".pb_popover_tooltip")
120
+ if (popoverTooltip && !popoverTooltip.classList.contains("show")) {
121
+ return false
122
+ }
123
+
105
124
  window.datePickerHelper(<%= object.date_picker_config %>, "<%= object.scroll_container %>")
106
125
  initQuickPickChangeListener(input)
126
+ disconnectPopoverWatch(input)
107
127
 
108
128
  return true
109
129
  }
110
130
 
131
+ const watchPopoverVisibility = () => {
132
+ const input = document.getElementById("<%= object.picker_id %>")
133
+ if (!input || input.dataset.pbDatePickerPopoverWatch) return
134
+
135
+ const popoverTooltip = input.closest(".pb_popover_tooltip")
136
+ if (!popoverTooltip) return
137
+
138
+ input.dataset.pbDatePickerPopoverWatch = "true"
139
+ const tryInitWhenVisible = () => {
140
+ if (!input.isConnected || !popoverTooltip.isConnected) {
141
+ disconnectPopoverWatch(input)
142
+ return
143
+ }
144
+
145
+ if (popoverTooltip.classList.contains("show")) {
146
+ loadDatePicker()
147
+ }
148
+ }
149
+
150
+ const observer = new MutationObserver(tryInitWhenVisible)
151
+ input._pbDatePickerPopoverObserver = observer
152
+ observer.observe(popoverTooltip, { attributes: true, attributeFilter: ["class"] })
153
+ tryInitWhenVisible()
154
+ }
155
+
111
156
  let attempts = 0
112
157
  const retryLoad = () => {
113
158
  if (loadDatePicker()) return
114
159
 
160
+ const input = document.getElementById("<%= object.picker_id %>")
161
+ if (input?.closest(".pb_popover_tooltip")) {
162
+ watchPopoverVisibility()
163
+ return
164
+ }
165
+
115
166
  if (attempts++ < 20) {
116
167
  setTimeout(retryLoad, 100)
117
168
  }
@@ -134,6 +185,12 @@
134
185
  if (<%= !object.custom_event_type.empty? %>) {
135
186
  window.addEventListener("<%= object.custom_event_type %>", retryLoad)
136
187
  }
188
+
189
+ document.addEventListener("turbo:before-cache", () => {
190
+ const input = document.getElementById("<%= object.picker_id %>")
191
+ disconnectPopoverWatch(input)
192
+ input?._flatpickr?.destroy()
193
+ })
137
194
  })()
138
195
  <% end %>
139
196
  <% end %>
@@ -6,6 +6,18 @@ import weekSelect from "flatpickr/dist/plugins/weekSelect/weekSelect"
6
6
  import timeSelectPlugin from './plugins/timeSelect'
7
7
  import quickPickPlugin from './plugins/quickPick'
8
8
  import { getAllIcons } from '../utilities/icons/allicons';
9
+ import {
10
+ announceFloatingKitOpen,
11
+ nextPortaledFloatingZIndex,
12
+ positionDropdownPortalToWrapper,
13
+ resolveFloatingOwnerId,
14
+ resolvePortaledFloatingZIndex,
15
+ resolvePortaledKitHost,
16
+ kitRequiresPortaledFloatingUi,
17
+ setFloatingOwnerAttribute,
18
+ subscribeFloatingKitOpen,
19
+ subscribeFloatingKitReposition,
20
+ } from '../utilities/floatingPortalHosts'
9
21
 
10
22
  const { angleDown, angleLeft, angleRight } = getAllIcons()
11
23
  const angleDownString = angleDown.string
@@ -41,7 +53,9 @@ type DatePickerConfig = {
41
53
  controlsEndId?: string,
42
54
  syncStartWith?: string,
43
55
  syncEndWith?: string,
44
- } & Pick<BaseOptions, "allowInput" | "defaultDate" | "enableTime" | "maxDate" | "minDate" | "mode" | "plugins" | "position" | "positionElement" >
56
+ /** React Dialog floating root; omit in Rails (DOM resolution only). */
57
+ dialogPortalTarget?: HTMLElement | null,
58
+ } & Pick<BaseOptions, "allowInput" | "defaultDate" | "enableTime" | "maxDate" | "minDate" | "mode" | "plugins" | "position" | "positionElement" | "inline" >
45
59
 
46
60
  const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HTMLElement) => {
47
61
  const noop = () => {
@@ -80,8 +94,92 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
80
94
  controlsEndId,
81
95
  syncStartWith,
82
96
  syncEndWith,
97
+ dialogPortalTarget,
98
+ inline = false,
83
99
  } = config
84
100
 
101
+ const inputElForPortal =
102
+ typeof document !== "undefined"
103
+ ? document.querySelector<HTMLElement>(`#${String(pickerId)}`)
104
+ : null
105
+ const kitRootForPortal = inputElForPortal?.closest(".pb_date_picker_kit") as HTMLElement | null
106
+ const portalHost =
107
+ !inline && kitRootForPortal && kitRequiresPortaledFloatingUi(kitRootForPortal)
108
+ ? resolvePortaledKitHost(kitRootForPortal, dialogPortalTarget ?? null)
109
+ : null
110
+
111
+ const floatingOwnerId = resolveFloatingOwnerId(kitRootForPortal)
112
+ const effectiveStatic = portalHost ? false : staticPosition
113
+
114
+ type DatePickerInputEl = HTMLElement & {
115
+ _flatpickr?: Instance
116
+ _pbDatePickerOpenUnsub?: () => void
117
+ }
118
+
119
+ const existingInput = document.querySelector(`#${String(pickerId)}`) as DatePickerInputEl | null
120
+ if (existingInput?._pbDatePickerOpenUnsub) {
121
+ existingInput._pbDatePickerOpenUnsub()
122
+ delete existingInput._pbDatePickerOpenUnsub
123
+ }
124
+ if (existingInput?._flatpickr) {
125
+ existingInput._flatpickr.destroy()
126
+ }
127
+
128
+ let portalAppendShell: HTMLElement | undefined
129
+
130
+ const removePortalShell = (): void => {
131
+ if (!portalHost) return
132
+
133
+ const shellSelector = `[data-pb-date-picker-floating-shell="${String(pickerId)}"]`
134
+ const shell =
135
+ portalAppendShell?.isConnected
136
+ ? portalAppendShell
137
+ : (portalHost.querySelector(shellSelector) as HTMLElement | undefined)
138
+
139
+ shell?.remove()
140
+ portalAppendShell = undefined
141
+ }
142
+
143
+ const ensurePortalShell = (): HTMLElement | undefined => {
144
+ if (!portalHost) return undefined
145
+ if (portalAppendShell?.isConnected) return portalAppendShell
146
+
147
+ const shellSelector = `[data-pb-date-picker-floating-shell="${String(pickerId)}"]`
148
+ portalAppendShell =
149
+ portalHost.querySelector(shellSelector) as HTMLElement | undefined
150
+
151
+ if (!portalAppendShell) {
152
+ portalAppendShell = document.createElement("div")
153
+ portalAppendShell.className = "pb_date_picker_floating_shell pb_date_picker_kit"
154
+ portalAppendShell.setAttribute(
155
+ "data-pb-date-picker-floating-shell",
156
+ String(pickerId),
157
+ )
158
+ portalAppendShell.addEventListener("mousedown", (e) => {
159
+ e.stopPropagation()
160
+ })
161
+ portalAppendShell.addEventListener("click", (e) => {
162
+ e.stopPropagation()
163
+ })
164
+ portalHost.appendChild(portalAppendShell)
165
+ }
166
+
167
+ setFloatingOwnerAttribute(portalAppendShell, floatingOwnerId)
168
+ return portalAppendShell
169
+ }
170
+
171
+ const mountCalendarInPortalShell = (fp: Instance): void => {
172
+ const shell = ensurePortalShell()
173
+ if (!shell?.isConnected || !fp.calendarContainer) return
174
+ if (fp.calendarContainer.parentElement !== shell) {
175
+ shell.appendChild(fp.calendarContainer)
176
+ }
177
+ }
178
+
179
+ let activePortalZIndex: string | undefined
180
+
181
+ let unsubscribeFloatingReposition: (() => void) | null = null
182
+
85
183
  // ===========================================================
86
184
  // | Hook Definitions |
87
185
  // ===========================================================
@@ -188,6 +286,12 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
188
286
  }
189
287
 
190
288
  const positionCalendarIfNeeded = (fp: Instance) => {
289
+ if (portalHost) {
290
+ mountCalendarInPortalShell(fp)
291
+ fp._positionCalendar()
292
+ return
293
+ }
294
+
191
295
  const cal = document.querySelector(`#cal-${pickerId}`) as HTMLElement
192
296
  if (!cal) return
193
297
 
@@ -197,7 +301,7 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
197
301
  const spaceAbove = inputRect.top
198
302
 
199
303
  if (spaceBelow < h + 10 && spaceAbove >= h + 10) {
200
- if (staticPosition) {
304
+ if (effectiveStatic) {
201
305
  cal.style.top = 'auto'
202
306
  cal.style.bottom = 'calc(100% + 5px)'
203
307
  } else {
@@ -205,7 +309,7 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
205
309
  cal.style.top = `${Math.max(10, inputRect.top - h - 5)}px`
206
310
  cal.style.left = `${inputRect.left}px`
207
311
  }
208
- } else if (staticPosition) {
312
+ } else if (effectiveStatic) {
209
313
  cal.style.top = ''
210
314
  cal.style.bottom = ''
211
315
  } else {
@@ -239,9 +343,10 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
239
343
  }
240
344
 
241
345
  // Attach / detach to / from scroll events
242
- const initialPicker = document.querySelector<HTMLElement & { [x: string]: any }>(`#${pickerId}`)._flatpickr
243
346
  const scrollEvent = () => {
244
- initialPicker._positionCalendar()
347
+ const fp = document.querySelector<HTMLElement & { _flatpickr?: Instance }>(`#${pickerId}`)
348
+ ?._flatpickr
349
+ fp?._positionCalendar()
245
350
  }
246
351
  function attachToScroll(scrollParent: string | HTMLElement) {
247
352
  document.querySelectorAll(scrollParent as string)[0]?.addEventListener("scroll", scrollEvent, { passive: true })
@@ -250,10 +355,23 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
250
355
  document.querySelectorAll(scrollParent as string)[0]?.removeEventListener("scroll", scrollEvent)
251
356
  }
252
357
 
358
+ const yearSelectId = `year-${pickerId}`
359
+
360
+ const yearDropdownFromCalendar = (fp: Instance): HTMLSelectElement | null => {
361
+ return fp.calendarContainer?.querySelector<HTMLSelectElement>(`#${yearSelectId}`) ?? null
362
+ }
363
+
364
+ const yearDropdownIsReady = (fp: Instance): boolean => {
365
+ const dropdown = yearDropdownFromCalendar(fp)
366
+ return dropdown !== null && dropdown.options.length > 0
367
+ }
368
+
253
369
  // two way binding
254
370
  const yearChangeHook = (fp: Instance) => {
255
- const yearInput = document.querySelector(`#year-${fp.input.id}`) as HTMLInputElement
371
+ const yearInput = yearDropdownFromCalendar(fp)
372
+ if (yearInput) {
256
373
  yearInput.value = fp.currentYear?.toString()
374
+ }
257
375
  }
258
376
 
259
377
  const handleDatePickerChange = (fp: Instance, selectedDates: Date[]) => {
@@ -355,7 +473,94 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
355
473
  : setMaxDate
356
474
 
357
475
  // End of Default Date + Min/Max Date Initialization Helper Functions section ----/
358
-
476
+ const resolveDatePickerPortalWrapper = (input: HTMLElement): HTMLElement | null => {
477
+ return (
478
+ (input.closest(".date_picker_input_wrapper") as HTMLElement | null) ??
479
+ (input.closest(".text_input_wrapper") as HTMLElement | null) ??
480
+ (input.closest(".input_wrapper") as HTMLElement | null) ??
481
+ (input.closest(".pb_date_picker_kit") as HTMLElement | null)
482
+ )
483
+ }
484
+
485
+ const portalPositionFn: ((self: Instance) => void) | undefined = portalHost
486
+ ? (self: Instance) => {
487
+ mountCalendarInPortalShell(self)
488
+ const shell = ensurePortalShell()
489
+ const cal = self.calendarContainer
490
+ if (!cal || !shell) return
491
+ const wrap = resolveDatePickerPortalWrapper(self.input)
492
+ const host = shell.parentElement as HTMLElement | null
493
+ if (!wrap || !host) return
494
+ positionDropdownPortalToWrapper({
495
+ panel: cal,
496
+ wrapperViewportRect: wrap.getBoundingClientRect(),
497
+ positionHost: host,
498
+ matchWrapperWidth: selectionType === "quickpick",
499
+ zIndex: activePortalZIndex,
500
+ })
501
+ }
502
+ : undefined
503
+
504
+ const setupYearMonthDropdowns = (fp: Instance) => {
505
+ if (yearDropdownIsReady(fp) || !fp.yearElements?.[0]?.parentElement) return
506
+
507
+ fp.yearElements[0].parentElement.innerHTML = `<select class="numInput cur-year" type="number" tabIndex="-1" aria-label="Year" id="${yearSelectId}"></select>`
508
+
509
+ let years = ''
510
+ if (yearAscending) {
511
+ for (let year = setMinYear; year <= setMaxYear; year++) {
512
+ years += `<option value="${year}">${year}</option>`
513
+ }
514
+ } else {
515
+ for (let year = setMaxYear; year >= setMinYear; year--) {
516
+ years += `<option value="${year}">${year}</option>`
517
+ }
518
+ }
519
+
520
+ const dropdown = yearDropdownFromCalendar(fp)
521
+ if (!dropdown) return
522
+
523
+ dropdown.innerHTML = years
524
+ dropdown.value = String(fp.currentYear)
525
+
526
+ dropdown.addEventListener('input', (e: Event & { target: { value: string }}) => {
527
+ fp.changeYear(Number(e.target.value))
528
+ })
529
+
530
+ if (fp.input.form) {
531
+ fp.input.form.addEventListener('reset', () => {
532
+ setTimeout(() => {
533
+ dropdown.value = String(fp.currentYear)
534
+ if (fp.monthsDropdownContainer) {
535
+ fp.monthsDropdownContainer.value = String(fp.currentMonth)
536
+ }
537
+
538
+ if (defaultDate) {
539
+ fp.setDate(defaultDate)
540
+ yearChangeHook(fp)
541
+ }
542
+ }, 0)
543
+ })
544
+ }
545
+
546
+ if (!dropdown.nextElementSibling?.classList.contains('year-dropdown-icon')) {
547
+ dropdown.insertAdjacentHTML('afterend', `<i class="year-dropdown-icon">${angleDownString}</i>`)
548
+ }
549
+ if (
550
+ fp.monthElements[0]?.parentElement &&
551
+ !fp.calendarContainer?.querySelector('.month-dropdown-icon')
552
+ ) {
553
+ fp.monthElements[0].insertAdjacentHTML('afterend', `<i class="month-dropdown-icon">${angleDownString}</i>`)
554
+ }
555
+ }
556
+
557
+ const assignCalendarId = (fp: Instance): void => {
558
+ const calEl = fp.calendarContainer ?? fp.innerContainer?.parentElement
559
+ if (calEl) {
560
+ calEl.id = `cal-${pickerId}`
561
+ }
562
+ }
563
+
359
564
  flatpickr(`#${pickerId}`, {
360
565
  allowInput,
361
566
  closeOnSelect,
@@ -372,6 +577,25 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
372
577
  mode,
373
578
  nextArrow: `<div style="height: 14px;">${angleRightString}</div>`,
374
579
  onOpen: [(_selectedDates, _dateStr, fp) => {
580
+ activePortalZIndex = portalHost
581
+ ? resolvePortaledFloatingZIndex(
582
+ portalHost,
583
+ nextPortaledFloatingZIndex(),
584
+ )
585
+ : undefined
586
+ if (portalHost) {
587
+ mountCalendarInPortalShell(fp)
588
+ }
589
+ if (portalAppendShell) {
590
+ portalAppendShell.style.zIndex = activePortalZIndex
591
+ setFloatingOwnerAttribute(portalAppendShell, floatingOwnerId)
592
+ }
593
+ if (fp.calendarContainer) {
594
+ fp.calendarContainer.style.zIndex = activePortalZIndex
595
+ setFloatingOwnerAttribute(fp.calendarContainer, floatingOwnerId)
596
+ }
597
+ announceFloatingKitOpen('date-picker', floatingOwnerId)
598
+
375
599
  // If defaultDate was out of range of a dev set min/max date, restore it when calendar opens (in situation where the input was manually cleared or the calendar was closed without selection)
376
600
  if (hasOutOfRangeDefault) {
377
601
  const dateObj = toDateObject(defaultDateValue)
@@ -402,15 +626,37 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
402
626
  positionCalendarIfNeeded(fp)
403
627
  }
404
628
  window.addEventListener('resize', resizeRepositionHandlerRef)
405
- if (!staticPosition && scrollContainer) attachToScroll(scrollContainer)
629
+ if (!effectiveStatic && scrollContainer) attachToScroll(scrollContainer)
630
+ if (portalHost) {
631
+ unsubscribeFloatingReposition = subscribeFloatingKitReposition(() => {
632
+ positionCalendarIfNeeded(fp)
633
+ })
634
+ }
635
+ assignCalendarId(fp)
406
636
  positionCalendarIfNeeded(fp)
637
+ setupYearMonthDropdowns(fp)
638
+ }],
639
+ onDestroy: [() => {
640
+ if (unsubscribeFloatingReposition) {
641
+ unsubscribeFloatingReposition()
642
+ unsubscribeFloatingReposition = null
643
+ }
644
+ if (resizeRepositionHandlerRef) {
645
+ window.removeEventListener('resize', resizeRepositionHandlerRef)
646
+ resizeRepositionHandlerRef = null
647
+ }
648
+ removePortalShell()
407
649
  }],
408
650
  onClose: [(selectedDates, dateStr, fp) => {
651
+ if (unsubscribeFloatingReposition) {
652
+ unsubscribeFloatingReposition()
653
+ unsubscribeFloatingReposition = null
654
+ }
409
655
  if (resizeRepositionHandlerRef) {
410
656
  window.removeEventListener('resize', resizeRepositionHandlerRef)
411
657
  resizeRepositionHandlerRef = null
412
658
  }
413
- if (!staticPosition && scrollContainer) detachFromScroll(scrollContainer as HTMLElement)
659
+ if (!effectiveStatic && scrollContainer) detachFromScroll(scrollContainer as HTMLElement)
414
660
 
415
661
  // If defaultDate was out of range and no date was selected, preserve the default date
416
662
  if (hasOutOfRangeDefault && (!selectedDates || selectedDates.length === 0)) {
@@ -440,15 +686,17 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
440
686
  yearChangeHook(fp)
441
687
  }],
442
688
  plugins: setPlugins(thisRangesEndToday, customQuickPickDates),
443
- position,
689
+ position: portalPositionFn ?? position,
444
690
  positionElement: getPositionElement(positionElement),
445
691
  prevArrow: `<div style="height: 14px;">${angleLeftString}</div>`,
446
- static: staticPosition,
692
+ static: effectiveStatic,
447
693
  })
448
694
 
449
695
  // Assign dynamically sourced flatpickr instance to variable
450
- const picker = document.querySelector<HTMLElement & { [x: string]: any }>(`#${pickerId}`)._flatpickr
451
- picker.innerContainer.parentElement.id = `cal-${pickerId}`
696
+ const picker = document.querySelector<HTMLElement & { [x: string]: any }>(`#${pickerId}`)?._flatpickr
697
+ if (!picker) return
698
+
699
+ assignCalendarId(picker)
452
700
 
453
701
  // If defaultDate was out of range, restore the original minDate/maxDate after initialization (defaultDate displayed, still cannot select dates outside the actual range via user provided minDate/maxDate constraints)
454
702
  if ((isBeforeMin || isAfterMax) && defaultDateValue) {
@@ -515,51 +763,7 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
515
763
  }, 10)
516
764
  }
517
765
 
518
- // replace year selector with dropdown
519
- picker.yearElements[0].parentElement.innerHTML = `<select class="numInput cur-year" type="number" tabIndex="-1" aria-label="Year" id="year-${pickerId}"></select>`
520
-
521
- // create html option tags for desired years
522
- let years = ''
523
- if (yearAscending) {
524
- for (let year = setMinYear; year <= setMaxYear; year++) {
525
- years += `<option value="${year}">${year}</option>`
526
- }
527
- } else {
528
- for (let year = setMaxYear; year >= setMinYear; year--) {
529
- years += `<option value="${year}">${year}</option>`
530
- }
531
- }
532
-
533
- // variablize each dropdown selector
534
- const dropdown = document.querySelector<HTMLElement & { [x: string]: any }>(`#year-${pickerId}`)
535
-
536
- // inject year options into dropdown and assign it the flatpickr's current year value
537
- dropdown.innerHTML = years
538
- dropdown.value = picker.currentYear
539
-
540
- // whenever a new year is selected from dropdown update flatpickr's current year value
541
- dropdown.addEventListener('input', (e: Event & { target: { value: string}}) => {
542
- picker.changeYear(Number(e.target.value))
543
- })
544
-
545
- // Reverse month and year dropdown reset on form.reset()
546
- if (picker.input.form) {
547
- picker.input.form.addEventListener('reset', () => {
548
- // Code block triggers after form.reset() is called and executed
549
- setTimeout(() => {
550
- dropdown.value = picker.currentYear
551
- if (picker.monthsDropdownContainer) {
552
- picker.monthsDropdownContainer.value = picker.currentMonth
553
- }
554
-
555
- /* Reset date picker to default value on form.reset() */
556
- if (defaultDate){
557
- picker.setDate(defaultDate)
558
- yearChangeHook(picker)
559
- }
560
- }, 0)
561
- })
562
- }
766
+ setupYearMonthDropdowns(picker)
563
767
 
564
768
  // === Automatic Sync Logic for 3 input range pattern===
565
769
 
@@ -661,11 +865,6 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
661
865
  }
662
866
  }
663
867
 
664
- // Adding dropdown icons to year and month select
665
- dropdown.insertAdjacentHTML('afterend', `<i class="year-dropdown-icon">${angleDownString}</i>`)
666
- if (picker.monthElements[0].parentElement) {
667
- return picker.monthElements[0].insertAdjacentHTML('afterend', `<i class="month-dropdown-icon">${angleDownString}</i>`)}
668
-
669
868
  // Remove readonly attribute for validation and or text input
670
869
  if (allowInput){
671
870
  picker.input.removeAttribute('readonly')
@@ -677,8 +876,55 @@ const datePickerHelper = (config: DatePickerConfig, scrollContainer: string | HT
677
876
  picker.input.style.cursor = 'pointer'
678
877
  }
679
878
 
879
+ const stopPointerForFlatpickrDocClose = (e: Event) => {
880
+ e.stopPropagation()
881
+ }
882
+
883
+ // Prevent flatpickr's document mousedown listener from treating the opening click as outside.
884
+ picker.input.addEventListener('mousedown', stopPointerForFlatpickrDocClose)
885
+ picker.input.addEventListener('touchstart', stopPointerForFlatpickrDocClose, { passive: true })
886
+
887
+ const calIconWrapper = document.querySelector(`#cal-icon-${pickerId}`) as HTMLElement | null
888
+ const isPointerInCalIcon = (e: MouseEvent) => {
889
+ if (!calIconWrapper) return false
890
+ const rect = calIconWrapper.getBoundingClientRect()
891
+ return (
892
+ e.clientX >= rect.left &&
893
+ e.clientX <= rect.right &&
894
+ e.clientY >= rect.top &&
895
+ e.clientY <= rect.bottom
896
+ )
897
+ }
898
+
899
+ const openDatePickerFromUi = (e: MouseEvent) => {
900
+ e.preventDefault()
901
+ e.stopPropagation()
902
+ if (picker.input.disabled) return
903
+ if (picker.isOpen) {
904
+ picker.close()
905
+ return
906
+ }
907
+ picker.open()
908
+ picker.input.focus()
909
+ }
910
+
911
+ const textInputWrapper = picker.input.closest('.text_input_wrapper') as HTMLElement | null
912
+ const inputWrapper = picker.input.closest('.input_wrapper') as HTMLElement | null
913
+ inputWrapper?.addEventListener('click', (e: MouseEvent) => {
914
+ if (e.target === picker.input) return
915
+ if (!isPointerInCalIcon(e) && !textInputWrapper?.contains(e.target as Node)) return
916
+ openDatePickerFromUi(e)
917
+ })
918
+
919
+ const pickerInput = picker.input as DatePickerInputEl
920
+ pickerInput._pbDatePickerOpenUnsub = subscribeFloatingKitOpen(({ kitKind }) => {
921
+ if (kitKind !== 'date-picker' && picker.isOpen) {
922
+ picker.close()
923
+ }
924
+ })
925
+
680
926
  // Fix event bubbling bug on wrapper
681
- document.querySelector(`#${pickerId}`).parentElement.addEventListener('click', (e) => e.stopPropagation())
927
+ document.querySelector(`#${pickerId}`)?.parentElement?.addEventListener('click', (e) => e.stopPropagation())
682
928
  }
683
929
 
684
930
  export default datePickerHelper