@aurodesignsystem-dev/auro-datetime 0.0.0-pr82.5 → 0.0.0-pr82.7

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.
package/demo/api.md CHANGED
@@ -284,6 +284,13 @@ How the `timeZone` attribute composes with the input depends on whether `value`
284
284
  - **`value` has an offset (or `Z`):** the offset anchors the absolute moment; `timeZone` is the display zone. Example: `value="2022-07-14T08:00:00-04:00" timezone="US/Pacific"` renders as `5:00 am` (8am Eastern → 5am Pacific).
285
285
  - **`value` has no offset:** the wall-clock is interpreted as being in `timeZone`, so display in that same zone matches the input verbatim. Example: `value="2022-07-14T08:00:00" timezone="US/Eastern"` renders as `8:00 am` regardless of where the viewer is. This is the recommended shape when consumers know the source zone but don't have an offset readily available (e.g. flight-schedule data keyed by airport).
286
286
 
287
+ #### Invalid `timeZone` and `locale` values
288
+
289
+ Both attributes are validated up-front. Invalid inputs do **not** crash the component — they fall back gracefully and log a `console.warn` (deduplicated per element):
290
+
291
+ - **Invalid `locale`** (e.g. `"xx-INVALID-tag"`) → falls back to `"en-US"`.
292
+ - **Invalid `timeZone`** (e.g. `"US/Pacfic"` typo) → falls back to behaving as if `timeZone` was not specified. With `value` set, that means wall-clock display from the input components — visually the same string for every viewer. With `value` unset, that means the current time in the viewer's machine zone. The warning surfaces the typo so the developer can fix it.
293
+
287
294
  ## Slot Examples
288
295
 
289
296
  ### Pre and Post Slots
@@ -143,6 +143,13 @@ class AuroDatetime extends i {
143
143
  */
144
144
  this._warnedLocales = new Set();
145
145
 
146
+ /**
147
+ * Tracks invalid timeZone inputs already warned about. Same dedup
148
+ * pattern as `_warnedLocales`.
149
+ * @private
150
+ */
151
+ this._warnedTimeZones = new Set();
152
+
146
153
  /**
147
154
  * @private
148
155
  */
@@ -180,6 +187,46 @@ class AuroDatetime extends i {
180
187
  }
181
188
  }
182
189
 
190
+ /**
191
+ * Validate an IANA timezone identifier. Invalid input warns once per
192
+ * unique value per element and resolves to `undefined`, which makes
193
+ * the rest of `_resolveInputDate` behave as if `timeZone` was never
194
+ * specified — i.e. wall-clock display from the input components.
195
+ *
196
+ * Without this guard, a typo like `"US/Pacfic"` would cause
197
+ * `Intl.DateTimeFormat` / `toLocaleString` to throw `RangeError` at
198
+ * render time and crash the component.
199
+ *
200
+ * Note: when `value` is also unset, the wall-clock branch resolves to
201
+ * "today's date in the viewer's machine zone," which is viewer-
202
+ * dependent. This narrow case is the only scenario where two viewers
203
+ * see different output for the same component; the warning surfaces
204
+ * the typo so the developer can fix it.
205
+ *
206
+ * @private
207
+ * @param {string | undefined} input - Consumer-supplied timezone.
208
+ * @returns {string | undefined} Valid timezone, or undefined on fallback.
209
+ */
210
+ _resolveTimeZone(input) {
211
+ if (!input) {
212
+ return undefined;
213
+ }
214
+ try {
215
+ // Constructing the formatter is enough to surface a RangeError;
216
+ // we don't need to format anything yet.
217
+ new Intl.DateTimeFormat(undefined, { timeZone: input });
218
+ return input;
219
+ } catch {
220
+ if (this._warnedTimeZones && !this._warnedTimeZones.has(input)) {
221
+ this._warnedTimeZones.add(input);
222
+ console.warn(
223
+ `auro-datetime: "${input}" is not a valid IANA timezone. Falling back to viewer-local.`,
224
+ );
225
+ }
226
+ return undefined;
227
+ }
228
+ }
229
+
183
230
  connectedCallback() {
184
231
  super.connectedCallback();
185
232
 
@@ -314,8 +361,13 @@ class AuroDatetime extends i {
314
361
  * timeZone option for `Intl.DateTimeFormat`.
315
362
  *
316
363
  * Behavior:
317
- * - No `value`: returns today's date. `timeZone` (if any) is honored.
318
- * - `value` invalid: warns and returns `{ date: null }`.
364
+ * - `value` omitted (null/undefined): returns today's date. `timeZone`
365
+ * (if any) is honored.
366
+ * - `value` invalid (including empty string ""): warns and returns
367
+ * `{ date: null }`. Empty is *not* the same as omitted — Lit reflects
368
+ * `value=""` as the empty string, which is not a valid ISO 8601
369
+ * form, so we surface it as a typo rather than silently rendering
370
+ * today's date.
319
371
  * - `timeZone` set + input has offset/Z: parses as an absolute moment
320
372
  * (offset honored) and asks `toLocaleString` to convert into the target zone.
321
373
  * - `timeZone` set + input has no offset: interprets the wall-clock as
@@ -328,8 +380,13 @@ class AuroDatetime extends i {
328
380
  * @returns {{ date: Date | null, timeZoneOption: string | undefined }}
329
381
  */
330
382
  _resolveInputDate() {
331
- if (!this.value) {
332
- return { date: new Date(), timeZoneOption: this.timeZone || undefined };
383
+ const resolvedTz = this._resolveTimeZone(this.timeZone);
384
+
385
+ // Only true "omitted" inputs (null/undefined) get the today's-date
386
+ // treatment. Empty string falls through to ISO validation, which will
387
+ // fail and produce the documented warn + empty render.
388
+ if (this.value == null) {
389
+ return { date: new Date(), timeZoneOption: resolvedTz };
333
390
  }
334
391
 
335
392
  const match = this.value.match(ISO_8601_REGEX);
@@ -342,27 +399,50 @@ class AuroDatetime extends i {
342
399
 
343
400
  const localPart = match[1];
344
401
  const hasOffset = Boolean(match[2]);
402
+
403
+ // The regex permits an offset/Z suffix on a bare date (e.g. "2020-09-22Z"
404
+ // or "2020-09-22-07:00"). Those forms are not valid ISO 8601 — an offset
405
+ // is only meaningful with a time component — and the wall-clock branch
406
+ // would silently drop the offset, producing surprising output. Reject
407
+ // them up front so the consumer sees a warning.
408
+ if (hasOffset && !localPart.includes("T")) {
409
+ console.warn(
410
+ `auro-datetime: "${this.value}" is not a valid ISO 8601 string (offset/Z requires a time component).`,
411
+ );
412
+ return { date: null, timeZoneOption: undefined };
413
+ }
414
+
345
415
  const normalized = localPart.includes("T")
346
416
  ? localPart
347
417
  : `${localPart}T00:00:00`;
348
418
 
349
- if (this.timeZone) {
350
- if (hasOffset) {
351
- return { date: new Date(this.value), timeZoneOption: this.timeZone };
419
+ // The regex only validates string shape, not value ranges — "2022-99-99"
420
+ // matches but produces an Invalid Date. Detect that and treat it like
421
+ // any other malformed input so render falls back to the documented
422
+ // empty output instead of "Invalid Date" or a thrown RangeError from
423
+ // downstream Intl calls.
424
+ const buildDate = () => {
425
+ if (resolvedTz && !hasOffset) {
426
+ return this._zonedWallClockToUtc(normalized, resolvedTz);
352
427
  }
353
- // No offset: the wall-clock is ambiguous without a zone. Treat it as
354
- // being in `timeZone` so the rendered output matches the input
355
- // regardless of where the viewer is.
356
- return {
357
- date: this._zonedWallClockToUtc(normalized, this.timeZone),
358
- timeZoneOption: this.timeZone,
359
- };
428
+ if (resolvedTz && hasOffset) {
429
+ return new Date(this.value);
430
+ }
431
+ // Wall-clock: keep the components in the input verbatim. JS parses
432
+ // bare "YYYY-MM-DD" as UTC midnight, which shifts the date in the
433
+ // viewer's timezone; appending a time forces local-time parsing.
434
+ return new Date(normalized);
435
+ };
436
+
437
+ const date = buildDate();
438
+ if (Number.isNaN(date.getTime())) {
439
+ console.warn(
440
+ `auro-datetime: "${this.value}" is not a valid ISO 8601 date.`,
441
+ );
442
+ return { date: null, timeZoneOption: undefined };
360
443
  }
361
444
 
362
- // Wall-clock: keep the components in the input verbatim. JS parses
363
- // bare "YYYY-MM-DD" as UTC midnight, which shifts the date in the
364
- // viewer's timezone; appending a time forces local-time parsing.
365
- return { date: new Date(normalized), timeZoneOption: undefined };
445
+ return { date, timeZoneOption: resolvedTz };
366
446
  }
367
447
 
368
448
  /**
@@ -0,0 +1,6 @@
1
+ import{LitElement as e,html as t}from"lit";class n{registerComponent(e,t){customElements.get(e)||customElements.define(e,class extends t{})}closestElement(e,t=this,n=(t,i=t&&t.closest(e))=>t&&t!==document&&t!==window?i||n(t.getRootNode().host):null){return n(t)}handleComponentTagRename(e,t){const n=t.toLowerCase();e.tagName.toLowerCase()!==n&&e.setAttribute(n,!0)}elementMatch(e,t){const n=t.toLowerCase();return e.tagName.toLowerCase()===n||e.hasAttribute(n)}getSlotText(e,t){const n=e.shadowRoot?.querySelector(`slot[name="${t}"]`);return(n?.assignedNodes({flatten:!0})||[]).map(e=>e.textContent?.trim()).join(" ").trim()||null}}const i=/^(\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)?)(Z|[+-]\d{2}:\d{2})?$/u;class a extends e{constructor(){super(),this._initializeDefaults()}_initializeDefaults(){this.weekday="short",this.month="short",this.locale="en-US",this._warnedLocales=new Set,this._warnedTimeZones=new Set,this._effectiveLocale=this._resolveLocale(this.locale),this.runtimeUtils=new n}_resolveLocale(e){if(!e)return"en-US";try{return Intl.getCanonicalLocales(e)[0]}catch{return this._warnedLocales&&!this._warnedLocales.has(e)&&(this._warnedLocales.add(e),console.warn(`auro-datetime: "${e}" is not a valid BCP 47 locale tag. Falling back to "en-US".`)),"en-US"}}_resolveTimeZone(e){if(e)try{return new Intl.DateTimeFormat(void 0,{timeZone:e}),e}catch{return void(this._warnedTimeZones&&!this._warnedTimeZones.has(e)&&(this._warnedTimeZones.add(e),console.warn(`auro-datetime: "${e}" is not a valid IANA timezone. Falling back to viewer-local.`)))}}connectedCallback(){super.connectedCallback(),this.dateTemplate={weekday:this.weekday,year:"numeric",month:this.month,day:"numeric"},this.timeTemplate={hour:"2-digit",minute:"2-digit"}}static get properties(){return{locale:{type:String},month:{type:String},timeZone:{type:String},type:{type:String},value:{type:String},weekday:{type:String}}}static register(e="auro-datetime"){n.prototype.registerComponent(e,a)}willUpdate(e){e.has("locale")&&(this._effectiveLocale=this._resolveLocale(this.locale))}firstUpdated(){this.runtimeUtils.handleComponentTagRename(this,"auro-datetime")}_zonedWallClockToUtc(e,t){const n=new Date(`${e}Z`),i=new Intl.DateTimeFormat("en-US",{timeZone:t,hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}).formatToParts(n),a=Object.fromEntries(i.filter(e=>"literal"!==e.type).map(e=>[e.type,e.value])),o=new Date(`${a.year}-${a.month}-${a.day}T${a.hour}:${a.minute}:${a.second}Z`),s=n.getTime()-o.getTime();return new Date(n.getTime()+s)}_resolveInputDate(){const e=this._resolveTimeZone(this.timeZone);if(null==this.value)return{date:new Date,timeZoneOption:e};const t=this.value.match(i);if(!t)return console.warn(`auro-datetime: "${this.value}" is not a valid ISO 8601 string.`),{date:null,timeZoneOption:void 0};const n=t[1],a=Boolean(t[2]);if(a&&!n.includes("T"))return console.warn(`auro-datetime: "${this.value}" is not a valid ISO 8601 string (offset/Z requires a time component).`),{date:null,timeZoneOption:void 0};const o=n.includes("T")?n:`${n}T00:00:00`,s=(()=>e&&!a?this._zonedWallClockToUtc(o,e):e&&a?new Date(this.value):new Date(o))();return Number.isNaN(s.getTime())?(console.warn(`auro-datetime: "${this.value}" is not a valid ISO 8601 date.`),{date:null,timeZoneOption:void 0}):{date:s,timeZoneOption:e}}humanDate(){const{date:e,timeZoneOption:t}=this._resolveInputDate();if(!e)return"";const n={...this.dateTemplate};return t&&(n.timeZone=t),e.toLocaleString(this._effectiveLocale,n)}humanDateConversion(){const{date:e,timeZoneOption:t}=this._resolveInputDate();if(!e)return"";const n={};switch(t&&(n.timeZone=t),this.type){case"day":n.day="numeric";break;case"month":n.month=this.month;break;case"year":n.year="numeric";break;case"weekday":n.weekday=this.weekday}return e.toLocaleString(this._effectiveLocale,n)}numericDate(){const{date:e,timeZoneOption:t}=this._resolveInputDate();if(!e)return"";const n={...this.dateTemplate,month:"numeric"};return Reflect.deleteProperty(n,"weekday"),t&&(n.timeZone=t),e.toLocaleString(this._effectiveLocale,n)}humanTime(){const{date:e,timeZoneOption:t}=this._resolveInputDate();if(!e)return"";const n={...this.timeTemplate};t&&(n.timeZone=t);const i=e.toLocaleString(this._effectiveLocale,n);return/[ap]\.?m\.?/iu.test(i)?i.replace(/^0+/u,"").toLowerCase():i}whichDate(){switch(this.type){case"date":default:return this.humanDate();case"time":return this.humanTime();case"year":case"month":case"weekday":case"day":return this.humanDateConversion();case"numeric":return this.numericDate()}}render(){return t`
2
+ <slot name="pre"></slot>
3
+ <span class="yield">${this.whichDate()}</span>
4
+ <slot name="post"></slot>
5
+ <slot></slot>
6
+ `}}export{a as A};
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export{A as AuroDatetime}from"./auro-datetime-BmzswYzJ.js";import"lit";
1
+ export{A as AuroDatetime}from"./auro-datetime-Cq0WDk5L.js";import"lit";
@@ -1 +1 @@
1
- import{A as r}from"./auro-datetime-BmzswYzJ.js";import"lit";r.register();
1
+ import{A as r}from"./auro-datetime-Cq0WDk5L.js";import"lit";r.register();
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "================================================================================"
8
8
  ],
9
9
  "name": "@aurodesignsystem-dev/auro-datetime",
10
- "version": "0.0.0-pr82.5",
10
+ "version": "0.0.0-pr82.7",
11
11
  "description": "auro-datetime HTML custom element",
12
12
  "repository": {
13
13
  "type": "git",
@@ -1,6 +0,0 @@
1
- import{LitElement as e,html as t}from"lit";class n{registerComponent(e,t){customElements.get(e)||customElements.define(e,class extends t{})}closestElement(e,t=this,n=(t,i=t&&t.closest(e))=>t&&t!==document&&t!==window?i||n(t.getRootNode().host):null){return n(t)}handleComponentTagRename(e,t){const n=t.toLowerCase();e.tagName.toLowerCase()!==n&&e.setAttribute(n,!0)}elementMatch(e,t){const n=t.toLowerCase();return e.tagName.toLowerCase()===n||e.hasAttribute(n)}getSlotText(e,t){const n=e.shadowRoot?.querySelector(`slot[name="${t}"]`);return(n?.assignedNodes({flatten:!0})||[]).map(e=>e.textContent?.trim()).join(" ").trim()||null}}const i=/^(\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)?)(Z|[+-]\d{2}:\d{2})?$/u;class a extends e{constructor(){super(),this._initializeDefaults()}_initializeDefaults(){this.weekday="short",this.month="short",this.locale="en-US",this._warnedLocales=new Set,this._effectiveLocale=this._resolveLocale(this.locale),this.runtimeUtils=new n}_resolveLocale(e){if(!e)return"en-US";try{return Intl.getCanonicalLocales(e)[0]}catch{return this._warnedLocales&&!this._warnedLocales.has(e)&&(this._warnedLocales.add(e),console.warn(`auro-datetime: "${e}" is not a valid BCP 47 locale tag. Falling back to "en-US".`)),"en-US"}}connectedCallback(){super.connectedCallback(),this.dateTemplate={weekday:this.weekday,year:"numeric",month:this.month,day:"numeric"},this.timeTemplate={hour:"2-digit",minute:"2-digit"}}static get properties(){return{locale:{type:String},month:{type:String},timeZone:{type:String},type:{type:String},value:{type:String},weekday:{type:String}}}static register(e="auro-datetime"){n.prototype.registerComponent(e,a)}willUpdate(e){e.has("locale")&&(this._effectiveLocale=this._resolveLocale(this.locale))}firstUpdated(){this.runtimeUtils.handleComponentTagRename(this,"auro-datetime")}_zonedWallClockToUtc(e,t){const n=new Date(`${e}Z`),i=new Intl.DateTimeFormat("en-US",{timeZone:t,hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}).formatToParts(n),a=Object.fromEntries(i.filter(e=>"literal"!==e.type).map(e=>[e.type,e.value])),o=new Date(`${a.year}-${a.month}-${a.day}T${a.hour}:${a.minute}:${a.second}Z`),s=n.getTime()-o.getTime();return new Date(n.getTime()+s)}_resolveInputDate(){if(!this.value)return{date:new Date,timeZoneOption:this.timeZone||void 0};const e=this.value.match(i);if(!e)return console.warn(`auro-datetime: "${this.value}" is not a valid ISO 8601 string.`),{date:null,timeZoneOption:void 0};const t=e[1],n=Boolean(e[2]),a=t.includes("T")?t:`${t}T00:00:00`;return this.timeZone?n?{date:new Date(this.value),timeZoneOption:this.timeZone}:{date:this._zonedWallClockToUtc(a,this.timeZone),timeZoneOption:this.timeZone}:{date:new Date(a),timeZoneOption:void 0}}humanDate(){const{date:e,timeZoneOption:t}=this._resolveInputDate();if(!e)return"";const n={...this.dateTemplate};return t&&(n.timeZone=t),e.toLocaleString(this._effectiveLocale,n)}humanDateConversion(){const{date:e,timeZoneOption:t}=this._resolveInputDate();if(!e)return"";const n={};switch(t&&(n.timeZone=t),this.type){case"day":n.day="numeric";break;case"month":n.month=this.month;break;case"year":n.year="numeric";break;case"weekday":n.weekday=this.weekday}return e.toLocaleString(this._effectiveLocale,n)}numericDate(){const{date:e,timeZoneOption:t}=this._resolveInputDate();if(!e)return"";const n={...this.dateTemplate,month:"numeric"};return Reflect.deleteProperty(n,"weekday"),t&&(n.timeZone=t),e.toLocaleString(this._effectiveLocale,n)}humanTime(){const{date:e,timeZoneOption:t}=this._resolveInputDate();if(!e)return"";const n={...this.timeTemplate};t&&(n.timeZone=t);const i=e.toLocaleString(this._effectiveLocale,n);return/[ap]\.?m\.?/iu.test(i)?i.replace(/^0+/u,"").toLowerCase():i}whichDate(){switch(this.type){case"date":default:return this.humanDate();case"time":return this.humanTime();case"year":case"month":case"weekday":case"day":return this.humanDateConversion();case"numeric":return this.numericDate()}}render(){return t`
2
- <slot name="pre"></slot>
3
- <span class="yield">${this.whichDate()}</span>
4
- <slot name="post"></slot>
5
- <slot></slot>
6
- `}}export{a as A};