@moni-labs/moni-ui 0.4.14 → 0.4.17

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.
@@ -153,11 +153,32 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
153
153
  this._originalSibling = null;
154
154
  this._isDragging = false;
155
155
  this._startY = 0;
156
- this._currentTranslationY = 0;
156
+ this._lastDeltaY = 0;
157
157
  this._sheetHeight = 0;
158
158
  this._defaultHeight = 0;
159
159
  this._justDragged = false;
160
160
  this._skipRequestClose = false;
161
+ this._isClosing = false;
162
+ }
163
+ /**
164
+ * Abre el bottom sheet.
165
+ */
166
+ show() {
167
+ this._isClosing = false;
168
+ this._isDragging = false;
169
+ this._lastDeltaY = 0;
170
+ if (this._dialog) {
171
+ this._dialog.classList.remove('dragging', 'expanded');
172
+ this._dialog.style.transform = '';
173
+ this._dialog.style.height = '';
174
+ }
175
+ this.open = true;
176
+ }
177
+ /**
178
+ * Cierra el bottom sheet ejecutando la animación fluida hacia abajo.
179
+ */
180
+ close() {
181
+ return this._animateAndClose();
161
182
  }
162
183
  willUpdate(changed) {
163
184
  if (changed.has('open') && this.open !== changed.get('open')) {
@@ -177,12 +198,18 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
177
198
  }
178
199
  }
179
200
  async _animateAndClose() {
201
+ if (this._isClosing)
202
+ return false;
203
+ if (!this.open)
204
+ return false;
205
+ this._isClosing = true;
180
206
  this._skipRequestClose = true;
181
207
  const allowed = emitMoniEvent(this, 'moni-request-close', { cancelable: true });
182
208
  this._skipRequestClose = false;
183
- if (!allowed)
209
+ if (!allowed) {
210
+ this._isClosing = false;
184
211
  return false;
185
- this.dispatchEvent(new Event('close', { bubbles: true, composed: true }));
212
+ }
186
213
  emitMoniEvent(this, 'moni-close');
187
214
  this._dialog.classList.remove('expanded');
188
215
  this._dialog.style.transform = 'translateY(100%)';
@@ -195,10 +222,18 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
195
222
  this._dialog.addEventListener('transitionend', onEnd);
196
223
  setTimeout(onEnd, 300);
197
224
  });
198
- this._dialog.style.transform = '';
225
+ this._isClosing = false;
226
+ this._isDragging = false;
227
+ this._lastDeltaY = 0;
228
+ if (this._dialog) {
229
+ this._dialog.classList.remove('dragging', 'expanded');
230
+ this._dialog.style.transform = '';
231
+ this._dialog.style.height = '';
232
+ }
199
233
  this._skipRequestClose = true;
200
234
  this.open = false;
201
235
  this._skipRequestClose = false;
236
+ this.dispatchEvent(new Event('close', { bubbles: true, composed: true }));
202
237
  emitMoniEvent(this, 'moni-closed');
203
238
  return true;
204
239
  }
@@ -234,14 +269,22 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
234
269
  this._isDragging = true;
235
270
  this._startY = e.clientY; // Almacenamos la coordenada inicial Y para calcular el delta luego
236
271
  this._sheetHeight = this._dialog.getBoundingClientRect().height; // Capturamos la altura actual
237
- this._currentTranslationY = 0;
272
+ this._lastDeltaY = 0;
238
273
  const isExpanded = this._dialog.classList.contains('expanded');
239
274
  // Si no estaba expandido previamente, guardamos su altura por defecto para usarla como punto de retorno.
240
275
  if (!isExpanded) {
241
276
  this._defaultHeight = this._sheetHeight;
242
277
  }
278
+ else if (!this._defaultHeight) {
279
+ this._defaultHeight = Math.min(this._sheetHeight * 0.6, 384);
280
+ }
243
281
  // setPointerCapture enruta todos los eventos subsecuentes a este target
244
- target.setPointerCapture(e.pointerId);
282
+ try {
283
+ target.setPointerCapture(e.pointerId);
284
+ }
285
+ catch {
286
+ // Ignorar si el navegador ya maneja el puntero de forma nativa
287
+ }
245
288
  this._dialog.classList.add('dragging'); // Desactiva las transiciones CSS para un seguimiento suave 1:1
246
289
  }
247
290
  /**
@@ -254,6 +297,7 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
254
297
  return;
255
298
  // Diferencia en píxeles desde donde se inició el click
256
299
  const deltaY = e.clientY - this._startY;
300
+ this._lastDeltaY = deltaY;
257
301
  const isExpanded = this._dialog.classList.contains('expanded');
258
302
  const maxHeight = this._getMaxHeightPx();
259
303
  if (isExpanded) {
@@ -273,7 +317,6 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
273
317
  else {
274
318
  if (deltaY > 0) {
275
319
  // Arrastrando hacia abajo desde su estado inicial: lo empujamos visualmente hacia abajo (translate)
276
- this._currentTranslationY = deltaY;
277
320
  this._dialog.style.transform = `translateY(${deltaY}px)`;
278
321
  this._dialog.style.height = '';
279
322
  }
@@ -288,16 +331,25 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
288
331
  /**
289
332
  * Finaliza la interacción de arrastre, evaluando si el bottom-sheet debe
290
333
  * expandirse, regresar a su estado original o cerrarse completamente según la
291
- * inercia o la distancia arrastrada.
334
+ * inercia, la distancia arrastrada o si se soltó fuera/hacia la parte inferior de la pantalla.
292
335
  */
293
336
  _onPointerUp(e) {
294
337
  if (!this._isDragging)
295
338
  return;
296
339
  const target = e.target;
297
- target.releasePointerCapture(e.pointerId); // Liberamos el control del puntero
340
+ try {
341
+ if (target && target.hasPointerCapture?.(e.pointerId)) {
342
+ target.releasePointerCapture(e.pointerId);
343
+ }
344
+ }
345
+ catch {
346
+ // Ignorar si la captura ya fue liberada o perdida por el navegador
347
+ }
298
348
  this._isDragging = false;
299
349
  this._dialog.classList.remove('dragging'); // Reactivamos las transiciones CSS
300
- const deltaY = e.clientY - this._startY;
350
+ // Evaluamos la posición exacta al momento de soltar
351
+ const hasValidY = typeof e.clientY === 'number' && e.clientY > 0;
352
+ const deltaY = hasValidY ? (e.clientY - this._startY) : this._lastDeltaY;
301
353
  // Si se movió más de 5px, bloqueamos clics accidentales para evitar disparar eventos subyacentes
302
354
  if (Math.abs(deltaY) > 5) {
303
355
  this._justDragged = true;
@@ -307,11 +359,18 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
307
359
  }
308
360
  const isExpanded = this._dialog.classList.contains('expanded');
309
361
  const maxHeight = this._getMaxHeightPx();
362
+ const defaultHeight = this._defaultHeight || (this._sheetHeight * 0.6) || 300;
363
+ // Solo se considera off-screen si al SOLTAR el puntero se encuentra en o más allá del límite inferior
364
+ const isOffScreenBottom = hasValidY && typeof window !== 'undefined' && e.clientY >= (window.innerHeight - 30);
310
365
  if (isExpanded) {
311
366
  // Evaluamos la altura final tras soltar el ratón/dedo
312
367
  const finalHeight = this._sheetHeight - deltaY;
313
- if (finalHeight < this._defaultHeight - 80) {
314
- // Si se arrastró significativamente por debajo de la altura por defecto, lo cerramos
368
+ const shouldClose = isOffScreenBottom ||
369
+ deltaY > this._sheetHeight * 0.4 ||
370
+ finalHeight < (defaultHeight - 80) ||
371
+ finalHeight < 100;
372
+ if (shouldClose) {
373
+ // Si se soltó significativamente hacia abajo o fuera de pantalla, lo cerramos
315
374
  this._animateAndClose().then((closed) => {
316
375
  if (!closed) {
317
376
  // Si se canceló el evento, regresamos a su lugar
@@ -320,20 +379,20 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
320
379
  }
321
380
  });
322
381
  }
323
- else if (finalHeight < (maxHeight + this._defaultHeight) / 2) {
382
+ else if (finalHeight < (maxHeight + defaultHeight) / 2) {
324
383
  // Si se soltó a medio camino, lo colapsamos a su altura normal
325
384
  this._dialog.classList.remove('expanded');
326
385
  this._dialog.style.transform = '';
327
386
  this._dialog.style.height = '';
328
387
  }
329
388
  else {
330
- // Si no se arrastró lo suficiente hacia abajo, retorna a estado expandido (snap back)
389
+ // Si se subió de nuevo hacia arriba o no se arrastró suficiente, retorna a estado expandido (snap back)
331
390
  this._dialog.style.transform = '';
332
391
  this._dialog.style.height = '';
333
392
  }
334
393
  }
335
394
  else {
336
- if (deltaY < 0) {
395
+ if (deltaY < 0 && !isOffScreenBottom) {
337
396
  // Evaluamos el arrastre hacia arriba desde el estado colapsado
338
397
  const finalHeight = this._sheetHeight - deltaY;
339
398
  if (finalHeight > this._sheetHeight + 80) {
@@ -344,8 +403,11 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
344
403
  this._dialog.style.height = '';
345
404
  }
346
405
  else {
347
- // Evaluamos el arrastre hacia abajo para cerrar el componente
348
- if (this._currentTranslationY > 80) {
406
+ // Evaluamos si al soltar superó el umbral de descarte o se soltó fuera de la pantalla
407
+ const threshold = Math.min(80, this._sheetHeight * 0.25);
408
+ const shouldClose = isOffScreenBottom ||
409
+ deltaY > threshold;
410
+ if (shouldClose) {
349
411
  this._animateAndClose().then((closed) => {
350
412
  if (!closed) {
351
413
  this._dialog.style.transform = '';
@@ -353,26 +415,31 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
353
415
  });
354
416
  }
355
417
  else {
356
- // Regresa a su estado colapsado (snap back al inicio)
418
+ // Si el usuario bajó pero subió de nuevo antes de soltar (o no superó el umbral), regresa a abierto
357
419
  this._dialog.style.transform = '';
358
420
  }
359
421
  }
360
422
  }
423
+ this._lastDeltaY = 0;
361
424
  }
362
425
  /**
363
- * Aborta limpiamente la interacción de arrastre (Pointer Cancel).
364
- * Se dispara cuando el SO interrumpe el touch (ej: entrada de una llamada)
365
- * o si el navegador toma el control para un scroll nativo imprevisto.
426
+ * Maneja la cancelación del puntero o interrupción del SO.
427
+ * Si el usuario ya lo había arrastrado hacia abajo o hacia fuera de la pantalla,
428
+ * evalúa el cierre en lugar de simplemente cancelar y dejar la hoja colgada.
366
429
  */
367
430
  _onPointerCancel(e) {
368
- if (!this._isDragging)
369
- return;
370
- const target = e.target;
371
- target.releasePointerCapture(e.pointerId);
372
- this._isDragging = false;
373
- this._dialog.classList.remove('dragging');
374
- this._dialog.style.transform = '';
375
- this._dialog.style.height = '';
431
+ if (this._isDragging) {
432
+ this._onPointerUp(e);
433
+ }
434
+ }
435
+ /**
436
+ * Se ejecuta cuando se pierde la captura de puntero (por ejemplo al arrastrar fuera
437
+ * del viewport del navegador o perder el foco de la ventana).
438
+ */
439
+ _onLostPointerCapture(e) {
440
+ if (this._isDragging) {
441
+ this._onPointerUp(e);
442
+ }
376
443
  }
377
444
  /**
378
445
  * Handler de clicks en la superficie del modal (Backdrop).
@@ -410,6 +477,14 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
410
477
  }
411
478
  if (changed.has('open')) {
412
479
  if (this.open) {
480
+ this._isClosing = false;
481
+ this._isDragging = false;
482
+ this._lastDeltaY = 0;
483
+ if (this._dialog) {
484
+ this._dialog.classList.remove('dragging', 'expanded');
485
+ this._dialog.style.transform = '';
486
+ this._dialog.style.height = '';
487
+ }
413
488
  if (changed.get('open') !== undefined) {
414
489
  emitMoniEvent(this, 'moni-open');
415
490
  }
@@ -424,8 +499,13 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
424
499
  }
425
500
  }
426
501
  else {
502
+ this._isClosing = false;
503
+ this._isDragging = false;
504
+ this._lastDeltaY = 0;
427
505
  if (this._dialog) {
428
- this._dialog.classList.remove('expanded');
506
+ this._dialog.classList.remove('expanded', 'dragging');
507
+ this._dialog.style.transform = '';
508
+ this._dialog.style.height = '';
429
509
  }
430
510
  if (this.positioning === 'body' && this._originalParent && this.parentNode === document.body) {
431
511
  this._originalParent.insertBefore(this, this._originalSibling);
@@ -433,6 +513,60 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
433
513
  this._originalSibling = null;
434
514
  }
435
515
  }
516
+ // Sincroniza el estado declarativo (open/modal) con la API
517
+ // imperativa del <dialog> nativo. Sin esto el ?open del template
518
+ // solo abre el diálogo como no-modal: sin top layer, sin
519
+ // ::backdrop y sin bloqueo del fondo. Mismo patrón que
520
+ // moni-dialog y moni-side-sheet, con guards typeof para
521
+ // entornos sin HTMLDialogElement (jsdom).
522
+ //
523
+ // NOTA: no se lee dialog.open como guarda porque render() ya
524
+ // aplicó ?open antes de updated() y siempre reflejaría el nuevo
525
+ // estado. Se decide por la transición (changed): close() sobre
526
+ // un diálogo ya cerrado es no-op por spec, igual que showModal()
527
+ // sobre uno ya modal lanza InvalidStateError (tolerado).
528
+ if (this._dialog) {
529
+ const nativo = this._dialog;
530
+ if (this.open) {
531
+ if (this.modal && typeof nativo.showModal === 'function') {
532
+ let yaEsModal = false;
533
+ try {
534
+ yaEsModal = this._dialog.matches(':modal');
535
+ }
536
+ catch {
537
+ yaEsModal = false;
538
+ }
539
+ if (!yaEsModal) {
540
+ try {
541
+ nativo.showModal();
542
+ }
543
+ catch {
544
+ // Otro actor ya lo abrió.
545
+ }
546
+ }
547
+ }
548
+ else if (!this.modal && typeof nativo.show === 'function') {
549
+ try {
550
+ nativo.show();
551
+ }
552
+ catch {
553
+ // Otro actor ya lo abrió.
554
+ }
555
+ }
556
+ else if ((this.modal && typeof nativo.showModal !== 'function') ||
557
+ (!this.modal && typeof nativo.show !== 'function')) {
558
+ this._dialog.open = true;
559
+ }
560
+ }
561
+ else if (typeof nativo.close === 'function') {
562
+ try {
563
+ nativo.close();
564
+ }
565
+ catch {
566
+ // Ya estaba cerrado.
567
+ }
568
+ }
569
+ }
436
570
  }
437
571
  }
438
572
  /**
@@ -486,6 +620,10 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
486
620
  transform: translateY(0);
487
621
  }
488
622
 
623
+ dialog::backdrop {
624
+ background-color: rgb(0 0 0 / 0.5);
625
+ }
626
+
489
627
  dialog.fixed {
490
628
  position: fixed;
491
629
  inset: auto 0 0 0;
@@ -608,6 +746,7 @@ let MoniBottomSheet = class MoniBottomSheet extends MoniElement {
608
746
  @pointermove=${this._onPointerMove}
609
747
  @pointerup=${this._onPointerUp}
610
748
  @pointercancel=${this._onPointerCancel}
749
+ @lostpointercapture=${this._onLostPointerCapture}
611
750
  >
612
751
  ${this.handle ? html `<div class="handle" aria-hidden="true"></div>` : nothing}
613
752
  <header part="header">
package/docs/api.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@moni-labs/moni-ui",
3
- "generatedAt": "2026-08-24T08:08:23.078Z",
3
+ "generatedAt": "2026-09-06T10:02:20.078Z",
4
4
  "componentCount": 49,
5
5
  "components": [
6
6
  {
@@ -327,7 +327,18 @@
327
327
  "reflect": true
328
328
  }
329
329
  ],
330
- "methods": [],
330
+ "methods": [
331
+ {
332
+ "name": "show",
333
+ "signature": "show()",
334
+ "description": "Abre el bottom sheet."
335
+ },
336
+ {
337
+ "name": "close",
338
+ "signature": "close()",
339
+ "description": "Cierra el bottom sheet ejecutando la animación fluida hacia abajo."
340
+ }
341
+ ],
331
342
  "events": [],
332
343
  "slots": [
333
344
  {
@@ -102,7 +102,8 @@ No declara eventos propios.
102
102
 
103
103
  ## Métodos públicos
104
104
 
105
- No expone métodos públicos adicionales.
105
+ - `show()` Abre el bottom sheet.
106
+ - `close()` — Cierra el bottom sheet ejecutando la animación fluida hacia abajo.
106
107
 
107
108
  ## CSS Parts
108
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moni-labs/moni-ui",
3
- "version": "0.4.14",
3
+ "version": "0.4.17",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Design system unificado para Moni — Material Design 3 Expressive, mobile-first.",