poetry-core 0.0.2
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/DESIGN.md +145 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/THIRD_PARTY_NOTICES.md +507 -0
- data/app/components/poetry/core/box/component.rb +82 -0
- data/app/components/poetry/core/component.rb +390 -0
- data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
- data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
- data/app/components/poetry/core/concerns/introspection.rb +533 -0
- data/app/components/poetry/core/concerns/options.rb +262 -0
- data/app/components/poetry/core/concerns/parts.rb +148 -0
- data/app/components/poetry/core/concerns/stimulus.rb +363 -0
- data/app/components/poetry/core/concerns/styles.rb +323 -0
- data/app/components/poetry/core/style.rb +150 -0
- data/app/components/poetry/core/wrapper/component.rb +90 -0
- data/app/javascript/poetry/core/accordion_controller.js +165 -0
- data/app/javascript/poetry/core/action_bar_controller.js +109 -0
- data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
- data/app/javascript/poetry/core/calendar_controller.js +406 -0
- data/app/javascript/poetry/core/carousel_controller.js +167 -0
- data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
- data/app/javascript/poetry/core/checked_controller.js +208 -0
- data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
- data/app/javascript/poetry/core/combobox_controller.js +1095 -0
- data/app/javascript/poetry/core/command_controller.js +510 -0
- data/app/javascript/poetry/core/context_menu_controller.js +204 -0
- data/app/javascript/poetry/core/date_field_controller.js +584 -0
- data/app/javascript/poetry/core/date_picker_controller.js +111 -0
- data/app/javascript/poetry/core/deferred_controller.js +111 -0
- data/app/javascript/poetry/core/dialog_controller.js +206 -0
- data/app/javascript/poetry/core/dismissable_controller.js +182 -0
- data/app/javascript/poetry/core/drawer_controller.js +365 -0
- data/app/javascript/poetry/core/file_input_controller.js +191 -0
- data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
- data/app/javascript/poetry/core/helpers/announce.js +228 -0
- data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
- data/app/javascript/poetry/core/helpers/collection.js +18 -0
- data/app/javascript/poetry/core/helpers/direction.js +15 -0
- data/app/javascript/poetry/core/helpers/escape.js +38 -0
- data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
- data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
- data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
- data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
- data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
- data/app/javascript/poetry/core/helpers/mask.js +283 -0
- data/app/javascript/poetry/core/helpers/portal.js +217 -0
- data/app/javascript/poetry/core/helpers/presence.js +210 -0
- data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
- data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
- data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
- data/app/javascript/poetry/core/helpers/state.js +80 -0
- data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
- data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
- data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
- data/app/javascript/poetry/core/hotkey_controller.js +55 -0
- data/app/javascript/poetry/core/hover_card_controller.js +431 -0
- data/app/javascript/poetry/core/index.js +218 -0
- data/app/javascript/poetry/core/mask_controller.js +500 -0
- data/app/javascript/poetry/core/menu_controller.js +1050 -0
- data/app/javascript/poetry/core/menubar_controller.js +329 -0
- data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
- data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
- data/app/javascript/poetry/core/number_field_controller.js +404 -0
- data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
- data/app/javascript/poetry/core/otp_controller.js +219 -0
- data/app/javascript/poetry/core/popover_controller.js +300 -0
- data/app/javascript/poetry/core/popper_controller.js +439 -0
- data/app/javascript/poetry/core/pressed_controller.js +74 -0
- data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
- data/app/javascript/poetry/core/radio_group_controller.js +197 -0
- data/app/javascript/poetry/core/resizable_controller.js +202 -0
- data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
- data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
- data/app/javascript/poetry/core/search_field_controller.js +95 -0
- data/app/javascript/poetry/core/select_controller.js +924 -0
- data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
- data/app/javascript/poetry/core/sheet_controller.js +24 -0
- data/app/javascript/poetry/core/sidebar_controller.js +272 -0
- data/app/javascript/poetry/core/slider_controller.js +419 -0
- data/app/javascript/poetry/core/state_controller.js +82 -0
- data/app/javascript/poetry/core/table_selection_controller.js +153 -0
- data/app/javascript/poetry/core/tabs_controller.js +151 -0
- data/app/javascript/poetry/core/tag_group_controller.js +134 -0
- data/app/javascript/poetry/core/toast_controller.js +228 -0
- data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
- data/app/javascript/poetry/core/toaster_controller.js +197 -0
- data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
- data/app/javascript/poetry/core/tooltip_controller.js +541 -0
- data/app/javascript/poetry/core/tree_controller.js +265 -0
- data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
- data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
- data/app/views/poetry/core/preview.html.erb +13 -0
- data/config/component_registry.yml +20 -0
- data/config/controllers_manifest.json +1528 -0
- data/config/importmap.rb +11 -0
- data/config/state_vocabulary.json +31 -0
- data/lib/active_model/type/list.rb +33 -0
- data/lib/active_model/type/symbol.rb +36 -0
- data/lib/poetry/core/check/stable_identity.rb +104 -0
- data/lib/poetry/core/check.rb +1445 -0
- data/lib/poetry/core/config.rb +286 -0
- data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
- data/lib/poetry/core/css/bem_merger.rb +45 -0
- data/lib/poetry/core/css/bem_reference.rb +87 -0
- data/lib/poetry/core/css/override_scan.rb +110 -0
- data/lib/poetry/core/css/resolver.rb +161 -0
- data/lib/poetry/core/css/safelist.rb +41 -0
- data/lib/poetry/core/css/tailwind_merger.rb +102 -0
- data/lib/poetry/core/css/template_classes.rb +102 -0
- data/lib/poetry/core/css/theme_coverage.rb +72 -0
- data/lib/poetry/core/css/var_coverage.rb +75 -0
- data/lib/poetry/core/css/verifier.rb +106 -0
- data/lib/poetry/core/design_lint.rb +720 -0
- data/lib/poetry/core/design_md/import.rb +250 -0
- data/lib/poetry/core/design_md.rb +461 -0
- data/lib/poetry/core/engine.rb +71 -0
- data/lib/poetry/core/errors.rb +53 -0
- data/lib/poetry/core/html/attributes.rb +667 -0
- data/lib/poetry/core/icons.rb +157 -0
- data/lib/poetry/core/llms_text.rb +354 -0
- data/lib/poetry/core/page_architectures.rb +304 -0
- data/lib/poetry/core/part_contract.rb +285 -0
- data/lib/poetry/core/preview/abstract.rb +121 -0
- data/lib/poetry/core/preview/base.rb +227 -0
- data/lib/poetry/core/preview/sidecarable.rb +136 -0
- data/lib/poetry/core/preview/template.rb +186 -0
- data/lib/poetry/core/recipe_items.rb +82 -0
- data/lib/poetry/core/registry.rb +363 -0
- data/lib/poetry/core/registry_address.rb +115 -0
- data/lib/poetry/core/registry_client.rb +217 -0
- data/lib/poetry/core/registry_installer.rb +234 -0
- data/lib/poetry/core/registry_items.rb +184 -0
- data/lib/poetry/core/skill_text.rb +288 -0
- data/lib/poetry/core/stable_id.rb +110 -0
- data/lib/poetry/core/stimulus/builder.rb +395 -0
- data/lib/poetry/core/stimulus/declarations.rb +444 -0
- data/lib/poetry/core/stimulus/manifest.rb +68 -0
- data/lib/poetry/core/stimulus/merger.rb +131 -0
- data/lib/poetry/core/stimulus_contract.rb +263 -0
- data/lib/poetry/core/tag_helper.rb +10 -0
- data/lib/poetry/core/template_compile.rb +74 -0
- data/lib/poetry/core/token_import.rb +317 -0
- data/lib/poetry/core/tokens/color.rb +215 -0
- data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
- data/lib/poetry/core/tokens/generator.rb +211 -0
- data/lib/poetry/core/tokens.rb +117 -0
- data/lib/poetry/core/version.rb +9 -0
- data/lib/poetry/core.rb +52 -0
- data/lib/poetry-core.rb +5 -0
- data/tokens/aliases.css +145 -0
- data/tokens/tailwind-theme.css +47 -0
- data/tokens/tokens.css +78 -0
- data/tokens/tokens.dtcg.json +779 -0
- data/vendor/floating-ui/LICENSE +20 -0
- data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
- data/vendor/shadcn-tailwind/LICENSE +21 -0
- data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
- data/vendor/shadcn-tailwind/tailwind.css +629 -0
- data/vendor/tw-animate-css/LICENSE +21 -0
- data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
- data/vendor/tw-animate-css/tw-animate.css +1 -0
- metadata +267 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import {
|
|
3
|
+
IncompleteDate, PAGE_STEP, resolveHourCycle
|
|
4
|
+
} from "@poetry/controllers/helpers/incomplete_date"
|
|
5
|
+
|
|
6
|
+
// The segmented date/time editor. The component renders a real native
|
|
7
|
+
// <input type=date|time> that IS the form value (no JS = native pickers;
|
|
8
|
+
// its ISO value format is exactly the wire contract), plus an empty group.
|
|
9
|
+
// This controller builds per-segment role=spinbutton spans from
|
|
10
|
+
// Intl.DateTimeFormat.formatToParts - so the LOCALE decides segment order
|
|
11
|
+
// and literals - and syncs segments -> native input eagerly whenever the
|
|
12
|
+
// value is complete and valid, constraining on blur (February 31st is
|
|
13
|
+
// representable mid-edit; commit clamps).
|
|
14
|
+
//
|
|
15
|
+
// Hour is stored in the locale's hour cycle with a separate dayPeriod
|
|
16
|
+
// segment; the cycle itself is INFERRED by formatting (two Intl bugs make
|
|
17
|
+
// resolvedOptions() untrustworthy - helpers/incomplete_date.js).
|
|
18
|
+
//
|
|
19
|
+
// Latin-digit typing only (the NumberField documented divergence); the
|
|
20
|
+
// DISPLAY uses Intl.NumberFormat, so locales that render non-latin digits
|
|
21
|
+
// still see their own numerals.
|
|
22
|
+
const EVENT_PREFIX = "poetry:date-field"
|
|
23
|
+
|
|
24
|
+
// iPadOS 13+ reports as Mac; maxTouchPoints tells them apart.
|
|
25
|
+
const isApplePlatform = () => /iP(hone|ad|od)/.test(navigator.userAgent) ||
|
|
26
|
+
(/Mac/.test(navigator.userAgent) && navigator.maxTouchPoints > 1)
|
|
27
|
+
|
|
28
|
+
const EDITABLE = {
|
|
29
|
+
date: ["year", "month", "day"],
|
|
30
|
+
time: ["hour", "minute", "second", "dayPeriod"]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default class DateFieldController extends Controller {
|
|
34
|
+
// The events this controller dispatches (manifest surface;
|
|
35
|
+
// events_declaration.test.js enforces the list stays honest).
|
|
36
|
+
static events = ["poetry:date-field:change"]
|
|
37
|
+
|
|
38
|
+
static targets = ["input", "group"]
|
|
39
|
+
static values = {
|
|
40
|
+
locale: { type: String, default: "" },
|
|
41
|
+
hourCycle: { type: String, default: "" }, // optional pin: h11|h12|h23|h24
|
|
42
|
+
seconds: { type: Boolean, default: false },
|
|
43
|
+
placeholder: { type: String, default: "" }, // ISO seed for arrow fills
|
|
44
|
+
labels: { type: Object, default: {} }, // segment-name fallbacks (i18n)
|
|
45
|
+
placeholders: { type: Object, default: {} } // segment placeholder text (i18n)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#value = null // IncompleteDate
|
|
49
|
+
#segments = [] // [{ element, type }]
|
|
50
|
+
#segmentDigits = {} // per-type display width, measured from the locale
|
|
51
|
+
#entered = "" // digit accumulator for the focused segment
|
|
52
|
+
#compositionSnapshot = null
|
|
53
|
+
#dayPeriodNames = null // { am, pm }
|
|
54
|
+
#onSelectionChange = () => this.#collapseSelectionInSegment()
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds the locale-driven segment row from formatToParts, marks the
|
|
58
|
+
* field enhanced, retires the native input from the tab order (it stays
|
|
59
|
+
* the form value and the constraint-validation carrier), and routes
|
|
60
|
+
* native invalid to the first empty segment.
|
|
61
|
+
*/
|
|
62
|
+
connect() {
|
|
63
|
+
if (!this.hasInputTarget || !this.hasGroupTarget) return
|
|
64
|
+
|
|
65
|
+
this.#value = new IncompleteDate(this.#resolvedHourCycle())
|
|
66
|
+
this.#readInput()
|
|
67
|
+
this.#buildSegments()
|
|
68
|
+
this.element.setAttribute("data-enhanced", "")
|
|
69
|
+
// The native input leaves the tab order (segments are the surface)
|
|
70
|
+
// but STAYS the form value and the constraint-validation carrier.
|
|
71
|
+
this.inputTarget.setAttribute("tabindex", "-1")
|
|
72
|
+
this.inputTarget.setAttribute("aria-hidden", "true")
|
|
73
|
+
|
|
74
|
+
// Native required validation fires on the (visually hidden) input;
|
|
75
|
+
// route the browser's focus request to the first empty segment.
|
|
76
|
+
this.inputTarget.addEventListener("invalid", this.#onInvalid)
|
|
77
|
+
document.addEventListener("selectionchange", this.#onSelectionChange)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Restores the native input's tab order / AT visibility and unwires the
|
|
82
|
+
* document listeners.
|
|
83
|
+
*/
|
|
84
|
+
disconnect() {
|
|
85
|
+
this.inputTarget?.removeEventListener("invalid", this.#onInvalid)
|
|
86
|
+
document.removeEventListener("selectionchange", this.#onSelectionChange)
|
|
87
|
+
this.element.removeAttribute("data-enhanced")
|
|
88
|
+
this.inputTarget?.removeAttribute("tabindex")
|
|
89
|
+
this.inputTarget?.removeAttribute("aria-hidden")
|
|
90
|
+
this.#segments = []
|
|
91
|
+
this.#segmentDigits = {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The group's click action: clicking blank space lands on the earliest
|
|
96
|
+
* unfilled segment (the focusLast heuristic, simplified - segments
|
|
97
|
+
* themselves stop propagation by matching first).
|
|
98
|
+
*
|
|
99
|
+
* @param {MouseEvent} event
|
|
100
|
+
*/
|
|
101
|
+
focusGap(event) {
|
|
102
|
+
if (event.target !== this.groupTarget) return
|
|
103
|
+
|
|
104
|
+
const target = this.#segments.find(({ element }) => element.hasAttribute("data-placeholder")) ??
|
|
105
|
+
this.#segments[0]
|
|
106
|
+
|
|
107
|
+
target?.element.focus()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- build ---
|
|
111
|
+
|
|
112
|
+
get #kind() {
|
|
113
|
+
return this.inputTarget.type === "time" ? "time" : "date"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#resolvedHourCycle() {
|
|
117
|
+
return resolveHourCycle(this.#locale(), this.hourCycleValue || null)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#locale() {
|
|
121
|
+
return this.localeValue || document.documentElement.lang || undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#formatterOptions() {
|
|
125
|
+
if (this.#kind === "date") return { year: "numeric", month: "numeric", day: "numeric" }
|
|
126
|
+
|
|
127
|
+
const options = { hour: "numeric", minute: "2-digit", hourCycle: this.#value.hourCycle }
|
|
128
|
+
|
|
129
|
+
if (this.secondsValue) options.second = "2-digit"
|
|
130
|
+
|
|
131
|
+
return options
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#buildSegments() {
|
|
135
|
+
const formatter = new Intl.DateTimeFormat(this.#locale(), this.#formatterOptions())
|
|
136
|
+
// A SINGLE-DIGIT probe (March 3rd, 9:45:05): each part's rendered
|
|
137
|
+
// length IS the locale's digit style - "3" under en-US, "03" under
|
|
138
|
+
// en-CA - so the display padding below can mirror it. A two-digit
|
|
139
|
+
// probe would read every locale as 2-digit.
|
|
140
|
+
const parts = formatter.formatToParts(new Date(2020, 2, 3, 9, 45, 5))
|
|
141
|
+
const editable = EDITABLE[this.#kind]
|
|
142
|
+
const fragment = document.createDocumentFragment()
|
|
143
|
+
|
|
144
|
+
this.#resolveDayPeriodNames()
|
|
145
|
+
|
|
146
|
+
// RTL correctness: the time run is wrapped in LRI/PDI isolates so
|
|
147
|
+
// minute:hour never renders reversed (harmless in LTR).
|
|
148
|
+
if (this.#kind === "time") fragment.appendChild(this.#literal(""))
|
|
149
|
+
|
|
150
|
+
for (const part of parts) {
|
|
151
|
+
const type = part.type === "dayperiod" ? "dayPeriod" : part.type
|
|
152
|
+
|
|
153
|
+
if (editable.includes(type)) {
|
|
154
|
+
const segment = this.#createSegment(type)
|
|
155
|
+
|
|
156
|
+
// The locale's own width for the padded types (year stays
|
|
157
|
+
// unpadded - "y" numeric everywhere real years are 4 digits).
|
|
158
|
+
if (type === "month" || type === "day" || type === "hour") {
|
|
159
|
+
this.#segmentDigits[type] = part.value.length
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.#segments.push({ element: segment, type })
|
|
163
|
+
fragment.appendChild(segment)
|
|
164
|
+
} else {
|
|
165
|
+
fragment.appendChild(this.#literal(part.value))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (this.#kind === "time") fragment.appendChild(this.#literal(""))
|
|
170
|
+
|
|
171
|
+
this.groupTarget.replaceChildren(fragment)
|
|
172
|
+
this.#labelSegments()
|
|
173
|
+
this.#segments.forEach(({ element, type }) => this.#renderSegment(element, type))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#literal(text) {
|
|
177
|
+
const span = document.createElement("span")
|
|
178
|
+
|
|
179
|
+
span.setAttribute("aria-hidden", "true")
|
|
180
|
+
span.setAttribute("data-slot", "date-field-literal")
|
|
181
|
+
span.textContent = text
|
|
182
|
+
return span
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#createSegment(type) {
|
|
186
|
+
const span = document.createElement("span")
|
|
187
|
+
const disabled = this.inputTarget.disabled
|
|
188
|
+
|
|
189
|
+
span.setAttribute("data-slot", "date-field-segment")
|
|
190
|
+
span.setAttribute("data-type", type)
|
|
191
|
+
|
|
192
|
+
// iOS VoiceOver cannot focus spinbuttons - segments fall back to
|
|
193
|
+
// role=textbox there, with the aria-value* surface stripped.
|
|
194
|
+
if (isApplePlatform() && navigator.maxTouchPoints > 0) span.setAttribute("role", "textbox")
|
|
195
|
+
else span.setAttribute("role", "spinbutton")
|
|
196
|
+
|
|
197
|
+
if (disabled) {
|
|
198
|
+
span.setAttribute("aria-disabled", "true")
|
|
199
|
+
} else {
|
|
200
|
+
span.setAttribute("tabindex", "0")
|
|
201
|
+
span.setAttribute("contenteditable", "true")
|
|
202
|
+
span.setAttribute("spellcheck", "false")
|
|
203
|
+
span.setAttribute("autocorrect", "off")
|
|
204
|
+
span.setAttribute("autocapitalize", "off")
|
|
205
|
+
span.setAttribute("enterkeyhint", "next")
|
|
206
|
+
|
|
207
|
+
if (type !== "dayPeriod") span.setAttribute("inputmode", "numeric")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (type !== "dayPeriod") {
|
|
211
|
+
// The bidi algorithm treats placeholder text and digits differently;
|
|
212
|
+
// without the embed, segments shift while deleting in RTL.
|
|
213
|
+
span.style.unicodeBidi = "embed"
|
|
214
|
+
span.style.direction = "ltr"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
span.style.caretColor = "transparent"
|
|
218
|
+
|
|
219
|
+
span.addEventListener("keydown", (event) => this.#onKeydown(event, span, type))
|
|
220
|
+
span.addEventListener("beforeinput", (event) => this.#onBeforeinput(event, span, type))
|
|
221
|
+
span.addEventListener("input", (event) => this.#onInput(event, span, type))
|
|
222
|
+
span.addEventListener("focus", () => { this.#entered = "" })
|
|
223
|
+
|
|
224
|
+
return span
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Segment names come from Intl.DisplayNames where available, the i18n
|
|
228
|
+
// fallback table otherwise - with the FIELD's label appended, because
|
|
229
|
+
// iOS VoiceOver does not announce group labels.
|
|
230
|
+
#labelSegments() {
|
|
231
|
+
let names = null
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
names = new Intl.DisplayNames(this.#locale(), { type: "dateTimeField" })
|
|
235
|
+
} catch { /* fallback table below */ }
|
|
236
|
+
|
|
237
|
+
const fieldLabel = this.#fieldLabelText()
|
|
238
|
+
|
|
239
|
+
this.#segments.forEach(({ element, type }, index) => {
|
|
240
|
+
const key = type === "dayPeriod" ? "dayPeriod" : type
|
|
241
|
+
let name = this.labelsValue[key] || key
|
|
242
|
+
|
|
243
|
+
if (names && type !== "dayPeriod") {
|
|
244
|
+
try { name = names.of(type) ?? name } catch { /* keep fallback */ }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
element.setAttribute("aria-label", fieldLabel ? `${name}, ${fieldLabel}` : name)
|
|
248
|
+
|
|
249
|
+
// Only the FIRST segment carries the description - anything more
|
|
250
|
+
// re-announces on every arrow press.
|
|
251
|
+
if (index === 0 && this.inputTarget.getAttribute("aria-describedby")) {
|
|
252
|
+
element.setAttribute("aria-describedby", this.inputTarget.getAttribute("aria-describedby"))
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
if (fieldLabel) this.groupTarget.setAttribute("aria-label", fieldLabel)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#fieldLabelText() {
|
|
260
|
+
const id = this.inputTarget.id
|
|
261
|
+
|
|
262
|
+
if (!id) return null
|
|
263
|
+
|
|
264
|
+
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id
|
|
265
|
+
|
|
266
|
+
return document.querySelector(`label[for="${escaped}"]`)?.textContent?.trim() ||
|
|
267
|
+
this.inputTarget.getAttribute("aria-label") || null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#resolveDayPeriodNames() {
|
|
271
|
+
if (this.#kind !== "time" || !this.#value.twelveHour) return
|
|
272
|
+
|
|
273
|
+
const formatter = new Intl.DateTimeFormat(this.#locale(), { hour: "numeric", hourCycle: this.#value.hourCycle })
|
|
274
|
+
const nameAt = (hour) =>
|
|
275
|
+
formatter.formatToParts(new Date(2020, 0, 1, hour)).find((part) => part.type === "dayPeriod")?.value
|
|
276
|
+
|
|
277
|
+
this.#dayPeriodNames = { am: nameAt(9) ?? "AM", pm: nameAt(21) ?? "PM" }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// --- input reflection ---
|
|
281
|
+
|
|
282
|
+
#readInput() {
|
|
283
|
+
const raw = this.inputTarget.value
|
|
284
|
+
|
|
285
|
+
if (this.#kind === "date") this.#value.setFromISODate(raw)
|
|
286
|
+
else this.#value.setFromISOTime(raw)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#placeholderFor(type) {
|
|
290
|
+
const seed = new IncompleteDate(this.#value.hourCycle)
|
|
291
|
+
const iso = this.placeholderValue
|
|
292
|
+
|
|
293
|
+
if (this.#kind === "date") {
|
|
294
|
+
if (!seed.setFromISODate(iso)) { seed.year = 2020; seed.month = 1; seed.day = 1 }
|
|
295
|
+
} else if (!seed.setFromISOTime(iso)) {
|
|
296
|
+
seed.setFromH23(12)
|
|
297
|
+
seed.minute = 0
|
|
298
|
+
seed.second = 0
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return seed[type] ?? seed.limits(type).min
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#renderSegment(element, type) {
|
|
305
|
+
const value = this.#value[type]
|
|
306
|
+
const filled = value !== null
|
|
307
|
+
|
|
308
|
+
element.toggleAttribute("data-placeholder", !filled)
|
|
309
|
+
|
|
310
|
+
if (type === "dayPeriod") {
|
|
311
|
+
element.textContent = filled
|
|
312
|
+
? (value === 0 ? this.#dayPeriodNames.am : this.#dayPeriodNames.pm)
|
|
313
|
+
: (this.placeholdersValue.dayPeriod || this.#dayPeriodNames?.am || "AM")
|
|
314
|
+
} else if (filled) {
|
|
315
|
+
element.textContent = this.#displayNumber(type, value)
|
|
316
|
+
} else {
|
|
317
|
+
element.textContent = this.placeholdersValue[type] || "––"
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (element.getAttribute("role") !== "spinbutton") return
|
|
321
|
+
|
|
322
|
+
const { min, max } = this.#value.limits(type)
|
|
323
|
+
|
|
324
|
+
element.setAttribute("aria-valuemin", String(min))
|
|
325
|
+
element.setAttribute("aria-valuemax", String(max))
|
|
326
|
+
|
|
327
|
+
if (filled) {
|
|
328
|
+
element.setAttribute("aria-valuenow", String(value))
|
|
329
|
+
element.setAttribute("aria-valuetext", this.#valueText(type, value))
|
|
330
|
+
} else {
|
|
331
|
+
element.removeAttribute("aria-valuenow")
|
|
332
|
+
element.setAttribute("aria-valuetext", this.labelsValue.empty || "Empty")
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#displayNumber(type, value) {
|
|
337
|
+
const digits = this.#segmentDigits[type] ?? (type === "minute" || type === "second" ? 2 : 1)
|
|
338
|
+
|
|
339
|
+
return new Intl.NumberFormat(this.#locale(), {
|
|
340
|
+
minimumIntegerDigits: digits, useGrouping: false
|
|
341
|
+
}).format(value)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Month announces "3 - March" (the screen-reader enrichment).
|
|
345
|
+
#valueText(type, value) {
|
|
346
|
+
if (type === "month") {
|
|
347
|
+
const name = new Intl.DateTimeFormat(this.#locale(), { month: "long" })
|
|
348
|
+
.format(new Date(2020, value - 1, 1))
|
|
349
|
+
|
|
350
|
+
return `${value} – ${name}`
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return this.#displayNumber(type, value)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// --- keyboard ---
|
|
357
|
+
|
|
358
|
+
#onKeydown(event, element, type) {
|
|
359
|
+
if (event.isComposing || this.inputTarget.readOnly) return
|
|
360
|
+
|
|
361
|
+
switch (event.key) {
|
|
362
|
+
case "ArrowUp": this.#step(event, element, type, 1); break
|
|
363
|
+
case "ArrowDown": this.#step(event, element, type, -1); break
|
|
364
|
+
case "PageUp": this.#step(event, element, type, PAGE_STEP[type] ?? 1, { round: true }); break
|
|
365
|
+
case "PageDown": this.#step(event, element, type, -(PAGE_STEP[type] ?? 1), { round: true }); break
|
|
366
|
+
case "Home": this.#jump(event, element, type, "min"); break
|
|
367
|
+
case "End": this.#jump(event, element, type, "max"); break
|
|
368
|
+
case "ArrowLeft": event.preventDefault(); this.#moveGeometric(element, -1); break
|
|
369
|
+
case "ArrowRight": event.preventDefault(); this.#moveGeometric(element, 1); break
|
|
370
|
+
case "Backspace":
|
|
371
|
+
case "Delete": event.preventDefault(); this.#erase(element, type); break
|
|
372
|
+
case "Enter": event.preventDefault(); this.#advanceFrom(element); break
|
|
373
|
+
default: this.#maybeDayPeriodKey(event, element, type)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#step(event, element, type, amount, options = {}) {
|
|
378
|
+
event.preventDefault()
|
|
379
|
+
this.#entered = ""
|
|
380
|
+
this.#value.cycle(type, amount, this.#placeholderFor(type), options)
|
|
381
|
+
this.#renderSegment(element, type)
|
|
382
|
+
this.#commitIfComplete()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#jump(event, element, type, edge) {
|
|
386
|
+
event.preventDefault()
|
|
387
|
+
this.#entered = ""
|
|
388
|
+
this.#value.set(type, this.#value.limits(type)[edge])
|
|
389
|
+
this.#renderSegment(element, type)
|
|
390
|
+
this.#commitIfComplete()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#erase(element, type) {
|
|
394
|
+
if (this.#value[type] === null) {
|
|
395
|
+
this.#moveGeometric(element, -1)
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const chopped = this.#entered.length > 1
|
|
400
|
+
? this.#entered.slice(0, -1)
|
|
401
|
+
: String(this.#value[type]).slice(0, -1)
|
|
402
|
+
|
|
403
|
+
this.#entered = chopped
|
|
404
|
+
|
|
405
|
+
if (chopped === "" || Number(chopped) === 0 || type === "dayPeriod") {
|
|
406
|
+
this.#value.clear(type)
|
|
407
|
+
this.#entered = ""
|
|
408
|
+
} else {
|
|
409
|
+
this.#value.set(type, Number(chopped))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this.#renderSegment(element, type)
|
|
413
|
+
this.#syncInput() // an erase can make the value incomplete - clear the wire
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#maybeDayPeriodKey(event, element, type) {
|
|
417
|
+
if (type !== "dayPeriod" || !this.#dayPeriodNames) return
|
|
418
|
+
if (event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) return
|
|
419
|
+
|
|
420
|
+
const typed = event.key.toLowerCase()
|
|
421
|
+
const { am, pm } = this.#dayPeriodNames
|
|
422
|
+
|
|
423
|
+
if (am.toLowerCase().startsWith(typed)) this.#value.set("dayPeriod", 0)
|
|
424
|
+
else if (pm.toLowerCase().startsWith(typed)) this.#value.set("dayPeriod", 1)
|
|
425
|
+
else return
|
|
426
|
+
|
|
427
|
+
event.preventDefault()
|
|
428
|
+
this.#renderSegment(element, "dayPeriod")
|
|
429
|
+
this.#commitIfComplete()
|
|
430
|
+
this.#advanceFrom(element)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// --- text input (beforeinput is the interception point: mobile numeric
|
|
434
|
+
// keyboards, swipe input, and per-segment paste all arrive here) ---
|
|
435
|
+
|
|
436
|
+
#onBeforeinput(event, element, type) {
|
|
437
|
+
if (this.inputTarget.readOnly) { event.preventDefault(); return }
|
|
438
|
+
|
|
439
|
+
// insertCompositionText cannot be canceled - snapshot, restore on
|
|
440
|
+
// input, then route the composed data (Android IME otherwise leaves
|
|
441
|
+
// real text inside the segment and breaks the DOM).
|
|
442
|
+
if (event.inputType === "insertCompositionText") {
|
|
443
|
+
this.#compositionSnapshot = element.textContent
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
event.preventDefault()
|
|
448
|
+
|
|
449
|
+
if (event.inputType.startsWith("delete")) {
|
|
450
|
+
this.#erase(element, type)
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
this.#routeText(event.data, element, type)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#onInput(event, element, type) {
|
|
458
|
+
if (this.#compositionSnapshot === null) return
|
|
459
|
+
|
|
460
|
+
element.textContent = this.#compositionSnapshot
|
|
461
|
+
this.#compositionSnapshot = null
|
|
462
|
+
this.#routeText(event.data, element, type)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#routeText(text, element, type) {
|
|
466
|
+
if (!text) return
|
|
467
|
+
|
|
468
|
+
for (const char of text) {
|
|
469
|
+
if (type === "dayPeriod") {
|
|
470
|
+
this.#maybeDayPeriodKey(
|
|
471
|
+
new KeyboardEvent("keydown", { key: char, cancelable: true }), element, type
|
|
472
|
+
)
|
|
473
|
+
} else if (/\d/.test(char)) {
|
|
474
|
+
this.#applyDigit(char, element, type)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#applyDigit(digit, element, type) {
|
|
480
|
+
const { min, max } = this.#value.limits(type)
|
|
481
|
+
let candidate = this.#entered + digit
|
|
482
|
+
|
|
483
|
+
// Typing past the maximum restarts from the last key:
|
|
484
|
+
// "3" then "5" in a month is 3, then 5 - never 35.
|
|
485
|
+
if (Number(candidate) > max) candidate = digit
|
|
486
|
+
|
|
487
|
+
this.#entered = candidate
|
|
488
|
+
const number = Number(candidate)
|
|
489
|
+
|
|
490
|
+
if (number >= min) this.#value.set(type, number)
|
|
491
|
+
else this.#value.clear(type)
|
|
492
|
+
|
|
493
|
+
this.#renderSegment(element, type)
|
|
494
|
+
|
|
495
|
+
// A pending sub-minimum entry ("0" in a month) shows what was typed.
|
|
496
|
+
if (number < min) element.textContent = candidate
|
|
497
|
+
|
|
498
|
+
this.#commitIfComplete()
|
|
499
|
+
|
|
500
|
+
// Auto-advance once no further digit can fit.
|
|
501
|
+
if (Number(candidate + "0") > max || candidate.length >= String(max).length) {
|
|
502
|
+
this.#advanceFrom(element)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// --- segment navigation (GEOMETRIC, not DOM order: numeric runs are
|
|
507
|
+
// LTR-embedded inside an RTL line, so arrows follow visual order) ---
|
|
508
|
+
|
|
509
|
+
#moveGeometric(element, direction) {
|
|
510
|
+
this.#entered = ""
|
|
511
|
+
|
|
512
|
+
const ordered = this.#segments
|
|
513
|
+
.map(({ element: seg }) => seg)
|
|
514
|
+
.filter((seg) => seg.hasAttribute("tabindex"))
|
|
515
|
+
.sort((a, b) => a.getBoundingClientRect().left - b.getBoundingClientRect().left)
|
|
516
|
+
const index = ordered.indexOf(element)
|
|
517
|
+
const next = ordered[index + direction]
|
|
518
|
+
|
|
519
|
+
next?.focus()
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#advanceFrom(element) {
|
|
523
|
+
this.#entered = ""
|
|
524
|
+
this.#moveGeometric(element, 1)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// --- commit ---
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The group's focusout action: leaving the whole group constrains
|
|
531
|
+
* (Feb 31 -> Feb 29) and commits.
|
|
532
|
+
*
|
|
533
|
+
* @param {FocusEvent} event
|
|
534
|
+
*/
|
|
535
|
+
settle(event) {
|
|
536
|
+
if (this.groupTarget.contains(event.relatedTarget)) return
|
|
537
|
+
|
|
538
|
+
this.#entered = ""
|
|
539
|
+
this.#value.constrain(EDITABLE[this.#kind])
|
|
540
|
+
this.#segments.forEach(({ element, type }) => this.#renderSegment(element, type))
|
|
541
|
+
this.#commitIfComplete()
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#commitIfComplete() {
|
|
545
|
+
if (this.#kind === "date" && this.#value.isComplete(["year", "month", "day"]) &&
|
|
546
|
+
!this.#value.isValidDate()) return // Feb 31 mid-edit: hold the wire
|
|
547
|
+
|
|
548
|
+
this.#syncInput()
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#syncInput() {
|
|
552
|
+
const iso = this.#kind === "date"
|
|
553
|
+
? this.#value.toISODate()
|
|
554
|
+
: this.#value.toISOTime(this.secondsValue)
|
|
555
|
+
const next = iso ?? ""
|
|
556
|
+
|
|
557
|
+
if (this.inputTarget.value === next) return
|
|
558
|
+
|
|
559
|
+
this.inputTarget.value = next
|
|
560
|
+
this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
|
|
561
|
+
this.dispatch("change", { prefix: EVENT_PREFIX, detail: { value: next } })
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#onInvalid = (event) => {
|
|
565
|
+
event.preventDefault()
|
|
566
|
+
const target = this.#segments.find(({ element }) => element.hasAttribute("data-placeholder")) ??
|
|
567
|
+
this.#segments[0]
|
|
568
|
+
|
|
569
|
+
target?.element.focus()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Android Chrome composition breaks if a selection RANGE lives inside a
|
|
573
|
+
// segment - collapse any selection that lands in one.
|
|
574
|
+
#collapseSelectionInSegment() {
|
|
575
|
+
const selection = document.getSelection?.()
|
|
576
|
+
|
|
577
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) return
|
|
578
|
+
|
|
579
|
+
const anchor = selection.anchorNode
|
|
580
|
+
const inSegment = anchor && this.#segments.some(({ element }) => element.contains(anchor))
|
|
581
|
+
|
|
582
|
+
if (inSegment) selection.collapseToStart()
|
|
583
|
+
}
|
|
584
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// The DatePicker glue: the thin coordinator between a Popover and
|
|
4
|
+
// the Calendar it wraps. The Calendar owns selection + the form value (its
|
|
5
|
+
// name: hidden input); this controller only reacts to the calendar's change
|
|
6
|
+
// event to (a) write the human-formatted date into the trigger label and
|
|
7
|
+
// (b) close the popover so the pick feels complete. Everything hard is
|
|
8
|
+
// already done by poetry--core--calendar and poetry--core--popover.
|
|
9
|
+
export default class DatePickerController extends Controller {
|
|
10
|
+
static targets = ["label", "input"]
|
|
11
|
+
static values = {
|
|
12
|
+
placeholder: { type: String, default: "Pick a date" },
|
|
13
|
+
mode: { type: String, default: "single" },
|
|
14
|
+
locale: { type: String, default: "en-US" }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The calendar-change action on the wrapper: writes the formatted date
|
|
19
|
+
* into the trigger label (and input, when present) and closes the
|
|
20
|
+
* popover so the pick feels complete. Range mode: the label joins the
|
|
21
|
+
* pair, a start-only pick shows one date, and the popover closes only
|
|
22
|
+
* once the range COMPLETES - never mid-range.
|
|
23
|
+
*
|
|
24
|
+
* @param {CustomEvent} event - detail carries value (single) or
|
|
25
|
+
* start/end (range) as ISO strings
|
|
26
|
+
*/
|
|
27
|
+
picked(event) {
|
|
28
|
+
if (this.modeValue === "range") {
|
|
29
|
+
const { start, end } = event.detail ?? {}
|
|
30
|
+
if (this.hasLabelTarget) {
|
|
31
|
+
this.labelTarget.textContent = start
|
|
32
|
+
? [start, end].filter(Boolean).map((iso) => this.#format(iso)).join(" – ")
|
|
33
|
+
: this.placeholderValue
|
|
34
|
+
}
|
|
35
|
+
if (start && end) this.#closePopover()
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const iso = event.detail?.value
|
|
40
|
+
|
|
41
|
+
if (this.hasLabelTarget) {
|
|
42
|
+
this.labelTarget.textContent = iso ? this.#format(iso) : this.placeholderValue
|
|
43
|
+
}
|
|
44
|
+
if (this.hasInputTarget && iso) this.inputTarget.value = this.#format(iso)
|
|
45
|
+
if (iso) this.#closePopover()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- the input variant (typed entry beside the calendar) ---
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The input's change action: a parseable date re-selects and re-months
|
|
52
|
+
* the calendar through its reactive values (the DOM is the store), and
|
|
53
|
+
* the calendar's own render refreshes the hidden ISO form value.
|
|
54
|
+
* Unparseable text changes nothing.
|
|
55
|
+
*/
|
|
56
|
+
inputChanged() {
|
|
57
|
+
if (!this.hasInputTarget) return
|
|
58
|
+
|
|
59
|
+
const parsed = new Date(this.inputTarget.value)
|
|
60
|
+
|
|
61
|
+
if (Number.isNaN(parsed.getTime())) return
|
|
62
|
+
|
|
63
|
+
const pad = (n) => String(n).padStart(2, "0")
|
|
64
|
+
const iso = `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}`
|
|
65
|
+
const calendar = this.#calendar()
|
|
66
|
+
|
|
67
|
+
if (!calendar) return
|
|
68
|
+
|
|
69
|
+
calendar.monthValue = iso.slice(0, 7)
|
|
70
|
+
calendar.selectedValue = iso
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The input's keydown action: ArrowDown opens the calendar.
|
|
75
|
+
*
|
|
76
|
+
* @param {KeyboardEvent} event
|
|
77
|
+
*/
|
|
78
|
+
inputKeydown(event) {
|
|
79
|
+
if (event.key !== "ArrowDown") return
|
|
80
|
+
|
|
81
|
+
event.preventDefault()
|
|
82
|
+
this.#popover()?.open()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#calendar() {
|
|
86
|
+
const root = this.element.querySelector('[data-controller~="poetry--core--calendar"]')
|
|
87
|
+
|
|
88
|
+
return root && this.application.getControllerForElementAndIdentifier(root, "poetry--core--calendar")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#popover() {
|
|
92
|
+
const root = this.element.querySelector('[data-controller~="poetry--core--popover"]')
|
|
93
|
+
|
|
94
|
+
return root && this.application.getControllerForElementAndIdentifier(root, "poetry--core--popover")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#format(iso) {
|
|
98
|
+
// Parse as UTC noon so the local-timezone render never rolls the day.
|
|
99
|
+
// Range labels use SHORT month names (the server formats %b for the
|
|
100
|
+
// same reason: two long-month dates outgrow the trigger).
|
|
101
|
+
const date = new Date(`${iso}T12:00:00Z`)
|
|
102
|
+
return date.toLocaleDateString(this.localeValue, {
|
|
103
|
+
year: "numeric", month: this.modeValue === "range" ? "short" : "long",
|
|
104
|
+
day: "numeric", timeZone: "UTC"
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#closePopover() {
|
|
109
|
+
this.#popover()?.close("item-press")
|
|
110
|
+
}
|
|
111
|
+
}
|