@eagami/ui 5.41.0 → 5.41.1

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.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders, provideEnvironmentInitializer, inject, CSP_NONCE, signal, effect, computed, Injectable, input, ChangeDetectionStrategy, Component, HostBinding, Directive, model, output, Injector, afterNextRender, DestroyRef, viewChild, ElementRef, untracked, HostListener, forwardRef, Renderer2, ViewContainerRef, TemplateRef, RendererStyleFlags2, ViewEncapsulation, contentChild, viewChildren, PLATFORM_ID, afterRenderEffect } from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, provideEnvironmentInitializer, inject, CSP_NONCE, signal, effect, computed, Injectable, input, ChangeDetectionStrategy, Component, HostBinding, Directive, model, output, DestroyRef, Injector, afterNextRender, viewChild, ElementRef, untracked, HostListener, forwardRef, Renderer2, ViewContainerRef, TemplateRef, RendererStyleFlags2, ViewEncapsulation, contentChild, viewChildren, PLATFORM_ID, afterRenderEffect } from '@angular/core';
3
3
  import { NgClass, NgComponentOutlet, NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
4
- import { NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
4
+ import { TouchedChangeEvent, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
5
5
 
6
6
  /** DI token carrying the consumer-supplied i18n configuration. */
7
7
  const EAGAMI_I18N_CONFIG = new InjectionToken('EAGAMI_I18N_CONFIG');
@@ -4210,32 +4210,151 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
4210
4210
  args: [{ selector: 'ea-field-messages', imports: [AlertCircleIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (error()) {\n <p\n class=\"ea-field-messages__message ea-field-messages__message--error\"\n [id]=\"id() + '-error'\"\n role=\"alert\">\n <ea-icon-alert-circle class=\"ea-field-messages__icon\" />\n {{ error() }}\n </p>\n} @else if (hint()) {\n <p\n class=\"ea-field-messages__message ea-field-messages__message--hint\"\n [id]=\"id() + '-hint'\">\n {{ hint() }}\n </p>\n}\n", styles: [":host{display:contents}.ea-field-messages__message{display:flex;align-items:center;gap:var(--space-1);font-size:max(var(--ea-field-messages-size, var(--text-helper-size)),.6875rem);font-weight:var(--text-helper-weight);line-height:var(--text-helper-lh)}.ea-field-messages__message--hint{color:var(--color-text-secondary)}.ea-field-messages__message--error{color:var(--color-error-default)}.ea-field-messages__icon{flex-shrink:0;font-size:1.2em;transform:translateY(-.0625em)}\n"] }]
4211
4211
  }], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
4212
4212
 
4213
+ /**
4214
+ * Tracks where the press behind the current `click` started and ended.
4215
+ *
4216
+ * A `click` is dispatched on the nearest common ancestor of the press and the
4217
+ * release, so a drag that begins on a panel and ends on the backdrop arrives as
4218
+ * a click on the backdrop. Dismissal handlers that only read `event.target`
4219
+ * therefore treat "select text in a dialog, release outside it" as an outside
4220
+ * click, and shut the dialog under the user. Consulting both ends of the press
4221
+ * keeps a dismissal to presses that stayed outside from start to finish.
4222
+ *
4223
+ * Both ends read `null` for a click with no pointer behind it (keyboard
4224
+ * activation, `element.click()`), which every check below treats as a plain
4225
+ * click on its target.
4226
+ *
4227
+ * It also knows whether a press is still in progress, so work that would move
4228
+ * content under the pointer can wait until the press has landed its click.
4229
+ */
4230
+ class PointerPressTracker {
4231
+ downTarget = null;
4232
+ upTarget = null;
4233
+ pressing = false;
4234
+ pressEndCallbacks = new Set();
4235
+ onDown = (event) => {
4236
+ this.downTarget = event.target;
4237
+ this.upTarget = null;
4238
+ this.pressing = true;
4239
+ };
4240
+ onUp = (event) => {
4241
+ this.upTarget = event.target;
4242
+ };
4243
+ // A click is already bound to its target by the time capture listeners run,
4244
+ // so work released from here can no longer redirect it
4245
+ onPressEnd = () => {
4246
+ this.pressing = false;
4247
+ const callbacks = [...this.pressEndCallbacks];
4248
+ this.pressEndCallbacks.clear();
4249
+ callbacks.forEach(callback => callback());
4250
+ };
4251
+ // A keyboard-driven click carries no pointer events of its own, and would
4252
+ // otherwise be read against whichever press came before it
4253
+ onKeydown = () => {
4254
+ this.downTarget = null;
4255
+ this.upTarget = null;
4256
+ this.onPressEnd();
4257
+ };
4258
+ constructor() {
4259
+ if (typeof document === 'undefined') {
4260
+ return;
4261
+ }
4262
+ // Capture phase, so a handler that stops propagation cannot hide the press
4263
+ document.addEventListener('pointerdown', this.onDown, true);
4264
+ document.addEventListener('pointerup', this.onUp, true);
4265
+ document.addEventListener('click', this.onPressEnd, true);
4266
+ document.addEventListener('pointercancel', this.onPressEnd, true);
4267
+ document.addEventListener('keydown', this.onKeydown, true);
4268
+ inject(DestroyRef).onDestroy(() => {
4269
+ document.removeEventListener('pointerdown', this.onDown, true);
4270
+ document.removeEventListener('pointerup', this.onUp, true);
4271
+ document.removeEventListener('click', this.onPressEnd, true);
4272
+ document.removeEventListener('pointercancel', this.onPressEnd, true);
4273
+ document.removeEventListener('keydown', this.onKeydown, true);
4274
+ });
4275
+ }
4276
+ /** True when either end of the press landed on `el` or inside it. */
4277
+ touchedInside(el) {
4278
+ if (!el) {
4279
+ return false;
4280
+ }
4281
+ return ((this.downTarget !== null && el.contains(this.downTarget)) ||
4282
+ (this.upTarget !== null && el.contains(this.upTarget)));
4283
+ }
4284
+ /** True when neither end of the press landed anywhere but `el` itself. */
4285
+ stayedOn(el) {
4286
+ if (!el) {
4287
+ return false;
4288
+ }
4289
+ return ((this.downTarget === null || this.downTarget === el) &&
4290
+ (this.upTarget === null || this.upTarget === el));
4291
+ }
4292
+ /**
4293
+ * Runs `callback` once the press in progress has landed its click or been
4294
+ * cancelled, or straight away when no press is in progress. Returns a
4295
+ * function that drops the pending call.
4296
+ */
4297
+ whenPressEnds(callback) {
4298
+ if (!this.pressing) {
4299
+ callback();
4300
+ return () => undefined;
4301
+ }
4302
+ this.pressEndCallbacks.add(callback);
4303
+ return () => this.pressEndCallbacks.delete(callback);
4304
+ }
4305
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PointerPressTracker, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4306
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PointerPressTracker, providedIn: 'root' });
4307
+ }
4308
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PointerPressTracker, decorators: [{
4309
+ type: Injectable,
4310
+ args: [{ providedIn: 'root' }]
4311
+ }], ctorParameters: () => [] });
4312
+
4213
4313
  /**
4214
4314
  * Derives a form control's display error from, in priority order: an explicit
4215
4315
  * `errorMsg`, a consumer `errorMessages` override, then the library's localized
4216
4316
  * default for the active `ValidationErrors` key. Auto-derived messages surface
4217
4317
  * only once the bound control is `invalid && touched`, matching the moment a
4218
- * user expects validation feedback. The control arrives as a signal so callers
4219
- * decide how it is resolved: from the host's own `NgControl` (CVA components)
4220
- * or from projected content (`<ea-form-field>`).
4318
+ * user expects validation feedback. A message revealed by a pointer press
4319
+ * moving focus away waits until that press has landed its click. The control
4320
+ * arrives as a signal so callers decide how it is resolved: from the host's own
4321
+ * `NgControl` (CVA components) or from projected content (`<ea-form-field>`).
4221
4322
  */
4222
4323
  function controlErrorStateFrom(control, config) {
4223
4324
  const i18n = inject(EagamiI18nService);
4325
+ const press = inject(PointerPressTracker);
4224
4326
  // Bumped on every status/touched change so the computed re-reads the control
4225
4327
  const revision = signal(0, /* @ts-ignore */
4226
4328
  ...(ngDevMode ? [{ debugName: "revision" }] : /* istanbul ignore next */ []));
4329
+ // The message a blur reveals grows the field and shifts the layout under the
4330
+ // pointer, so while the press behind that blur is down the click would land
4331
+ // on whatever moved into its place
4332
+ const revealPending = signal(false, /* @ts-ignore */
4333
+ ...(ngDevMode ? [{ debugName: "revealPending" }] : /* istanbul ignore next */ []));
4227
4334
  effect(onCleanup => {
4228
4335
  const ctrl = control();
4229
4336
  if (!ctrl) {
4230
4337
  return;
4231
4338
  }
4232
- const sub = ctrl.events.subscribe(() => revision.update(r => r + 1));
4233
- onCleanup(() => sub.unsubscribe());
4339
+ let dropPendingReveal = null;
4340
+ const sub = ctrl.events.subscribe(event => {
4341
+ if (event instanceof TouchedChangeEvent && event.touched) {
4342
+ dropPendingReveal?.();
4343
+ revealPending.set(true);
4344
+ dropPendingReveal = press.whenPressEnds(() => revealPending.set(false));
4345
+ }
4346
+ revision.update(r => r + 1);
4347
+ });
4348
+ onCleanup(() => {
4349
+ sub.unsubscribe();
4350
+ dropPendingReveal?.();
4351
+ revealPending.set(false);
4352
+ });
4234
4353
  });
4235
4354
  const autoError = computed(() => {
4236
4355
  revision();
4237
4356
  const ctrl = control();
4238
- if (!ctrl || !ctrl.invalid || !ctrl.touched || !ctrl.errors) {
4357
+ if (revealPending() || !ctrl || !ctrl.invalid || !ctrl.touched || !ctrl.errors) {
4239
4358
  return null;
4240
4359
  }
4241
4360
  return resolveValidationMessage(ctrl.errors, config.errorMessages(), i18n.messages());
@@ -4315,74 +4434,6 @@ function isRtl(element) {
4315
4434
  return getComputedStyle(element).direction === 'rtl';
4316
4435
  }
4317
4436
 
4318
- /**
4319
- * Tracks where the press behind the current `click` started and ended.
4320
- *
4321
- * A `click` is dispatched on the nearest common ancestor of the press and the
4322
- * release, so a drag that begins on a panel and ends on the backdrop arrives as
4323
- * a click on the backdrop. Dismissal handlers that only read `event.target`
4324
- * therefore treat "select text in a dialog, release outside it" as an outside
4325
- * click, and shut the dialog under the user. Consulting both ends of the press
4326
- * keeps a dismissal to presses that stayed outside from start to finish.
4327
- *
4328
- * Both ends read `null` for a click with no pointer behind it (keyboard
4329
- * activation, `element.click()`), which every check below treats as a plain
4330
- * click on its target.
4331
- */
4332
- class PointerPressTracker {
4333
- downTarget = null;
4334
- upTarget = null;
4335
- onDown = (event) => {
4336
- this.downTarget = event.target;
4337
- this.upTarget = null;
4338
- };
4339
- onUp = (event) => {
4340
- this.upTarget = event.target;
4341
- };
4342
- // A keyboard-driven click carries no pointer events of its own, and would
4343
- // otherwise be read against whichever press came before it
4344
- onKeydown = () => {
4345
- this.downTarget = null;
4346
- this.upTarget = null;
4347
- };
4348
- constructor() {
4349
- if (typeof document === 'undefined') {
4350
- return;
4351
- }
4352
- // Capture phase, so a handler that stops propagation cannot hide the press
4353
- document.addEventListener('pointerdown', this.onDown, true);
4354
- document.addEventListener('pointerup', this.onUp, true);
4355
- document.addEventListener('keydown', this.onKeydown, true);
4356
- inject(DestroyRef).onDestroy(() => {
4357
- document.removeEventListener('pointerdown', this.onDown, true);
4358
- document.removeEventListener('pointerup', this.onUp, true);
4359
- document.removeEventListener('keydown', this.onKeydown, true);
4360
- });
4361
- }
4362
- /** True when either end of the press landed on `el` or inside it. */
4363
- touchedInside(el) {
4364
- if (!el) {
4365
- return false;
4366
- }
4367
- return ((this.downTarget !== null && el.contains(this.downTarget)) ||
4368
- (this.upTarget !== null && el.contains(this.upTarget)));
4369
- }
4370
- /** True when neither end of the press landed anywhere but `el` itself. */
4371
- stayedOn(el) {
4372
- if (!el) {
4373
- return false;
4374
- }
4375
- return ((this.downTarget === null || this.downTarget === el) &&
4376
- (this.upTarget === null || this.upTarget === el));
4377
- }
4378
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PointerPressTracker, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4379
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PointerPressTracker, providedIn: 'root' });
4380
- }
4381
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PointerPressTracker, decorators: [{
4382
- type: Injectable,
4383
- args: [{ providedIn: 'root' }]
4384
- }], ctorParameters: () => [] });
4385
-
4386
4437
  /*
4387
4438
  * A modal `<dialog>` (`showModal()`) renders in the browser's top layer, which
4388
4439
  * paints above every z-index in the normal layer. An overlay portaled to