@tylertech/forge 3.10.4 → 3.10.5

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.
@@ -50,15 +50,13 @@ export function WithMoveable(base = class {
50
50
  return;
51
51
  }
52
52
  }
53
- // Ensure that the surface position stays within the bounds of the screen
54
- const newPosition = this._clampPosition(position, this._moveContext);
55
53
  // Only update the position if it actually changed
56
- if (!this._lastPosition || newPosition.x !== this._lastPosition.x || newPosition.y !== this._lastPosition.y) {
57
- const defaultPrevented = this._onMove(newPosition);
54
+ if (!this._lastPosition || position.x !== this._lastPosition.x || position.y !== this._lastPosition.y) {
55
+ const defaultPrevented = this._onMove(position);
58
56
  if (!defaultPrevented) {
59
- this._lastPosition = { ...newPosition };
60
- const newX = this._normalizePositionValue(newPosition.x);
61
- const newY = this._normalizePositionValue(newPosition.y);
57
+ this._lastPosition = { ...position };
58
+ const newX = this._normalizePositionValue(position.x);
59
+ const newY = this._normalizePositionValue(position.y);
62
60
  this._updatePosition(newX, newY);
63
61
  }
64
62
  }
@@ -168,3 +168,19 @@ export declare function checkVisibility(element: HTMLElement): boolean;
168
168
  * @param value - Whether to add or remove the state.
169
169
  */
170
170
  export declare function toggleState(internals: ElementInternals, state: string, value: boolean): void;
171
+ /**
172
+ * Determines if an element is clipped by the viewport bounds
173
+ * @param element The element to check.
174
+ * @returns `true` if the element is clipped by the viewport, otherwise `false`.
175
+ */
176
+ export declare function isElementClipped(element: HTMLElement | null): boolean;
177
+ /**
178
+ * Moves an element into the viewport by adjusting its position to ensure it's fully visible.
179
+ * @param element The element to move into view.
180
+ * @param options Configuration options for the viewport adjustment.
181
+ * @param options.padding The minimum distance from viewport edges (defaults to 8px).
182
+ * @returns `true` if the position was adjusted, otherwise `false`.
183
+ */
184
+ export declare function moveElementIntoViewport(element: HTMLElement | null, { padding }?: {
185
+ padding?: number | undefined;
186
+ }): boolean;
@@ -348,3 +348,70 @@ export function toggleState(internals, state, value) {
348
348
  }
349
349
  }
350
350
  }
351
+ /**
352
+ * Determines if an element is clipped by the viewport bounds
353
+ * @param element The element to check.
354
+ * @returns `true` if the element is clipped by the viewport, otherwise `false`.
355
+ */
356
+ export function isElementClipped(element) {
357
+ if (!element) {
358
+ return false;
359
+ }
360
+ const rect = element.getBoundingClientRect();
361
+ const viewportWidth = window.innerWidth;
362
+ const viewportHeight = window.innerHeight;
363
+ return rect.top < 0 || rect.left < 0 || rect.bottom > viewportHeight || rect.right > viewportWidth;
364
+ }
365
+ /**
366
+ * Moves an element into the viewport by adjusting its position to ensure it's fully visible.
367
+ * @param element The element to move into view.
368
+ * @param options Configuration options for the viewport adjustment.
369
+ * @param options.padding The minimum distance from viewport edges (defaults to 8px).
370
+ * @returns `true` if the position was adjusted, otherwise `false`.
371
+ */
372
+ export function moveElementIntoViewport(element, { padding = 8 } = {}) {
373
+ if (!element) {
374
+ return false;
375
+ }
376
+ const rect = element.getBoundingClientRect();
377
+ const viewportWidth = window.innerWidth;
378
+ const viewportHeight = window.innerHeight;
379
+ // Get current computed position values
380
+ const computedStyle = window.getComputedStyle(element);
381
+ const currentLeft = parseFloat(computedStyle.left) || 0;
382
+ const currentTop = parseFloat(computedStyle.top) || 0;
383
+ let newLeft = currentLeft;
384
+ let newTop = currentTop;
385
+ // Calculate the adjustments needed to bring the element into view
386
+ // Handle horizontal positioning
387
+ if (rect.left < 0) {
388
+ // Element extends beyond left edge - move it right
389
+ newLeft = currentLeft - rect.left + padding;
390
+ }
391
+ else if (rect.right > viewportWidth) {
392
+ // Element extends beyond right edge - move it left
393
+ newLeft = currentLeft - (rect.right - viewportWidth) - padding;
394
+ }
395
+ // Handle vertical positioning
396
+ if (rect.top < 0) {
397
+ // Element extends beyond top edge - move it down
398
+ newTop = currentTop - rect.top + padding;
399
+ }
400
+ else if (rect.bottom > viewportHeight) {
401
+ // Element extends beyond bottom edge - move it up
402
+ newTop = currentTop - (rect.bottom - viewportHeight) - padding;
403
+ }
404
+ // Ensure the element doesn't exceed viewport bounds after adjustment
405
+ // This prevents the element from being too large for the viewport
406
+ const maxLeft = viewportWidth - rect.width - padding;
407
+ const maxTop = viewportHeight - rect.height - padding;
408
+ newLeft = Math.max(padding, Math.min(newLeft, maxLeft));
409
+ newTop = Math.max(padding, Math.min(newTop, maxTop));
410
+ // Only apply position changes if they're different from current values
411
+ if (newLeft !== currentLeft || newTop !== currentTop) {
412
+ element.style.left = `${newLeft}px`;
413
+ element.style.top = `${newTop}px`;
414
+ return true;
415
+ }
416
+ return false;
417
+ }
@@ -25,10 +25,13 @@ export interface IDialogAdapter extends IBaseAdapter<IDialogComponent> {
25
25
  showBackdrop(): void;
26
26
  addSurfaceClass(className: string): void;
27
27
  removeSurfaceClass(className: string): void;
28
+ moveSurfaceIntoView(): void;
28
29
  addFullscreenListener(breakpoint: number, listener: (value: boolean) => void): void;
29
30
  removeFullscreenListener(listener: (value: boolean) => void): void;
30
31
  setAccessibleLabel(label: string): void;
31
32
  setAccessibleDescription(description: string): void;
33
+ isDialogCompletelyOffScreen(): boolean;
34
+ isSurfaceClipped(): boolean;
32
35
  }
33
36
  export declare class DialogAdapter extends BaseAdapter<IDialogComponent> implements IDialogAdapter {
34
37
  private _dialogElement;
@@ -59,9 +62,12 @@ export declare class DialogAdapter extends BaseAdapter<IDialogComponent> impleme
59
62
  showBackdrop(): void;
60
63
  addSurfaceClass(className: string): void;
61
64
  removeSurfaceClass(className: string): void;
65
+ moveSurfaceIntoView(): void;
62
66
  addFullscreenListener(breakpoint: number, listener: (value: boolean) => void): void;
63
67
  removeFullscreenListener(listener: (value: boolean) => void): void;
64
68
  setAccessibleLabel(label: string): void;
65
69
  setAccessibleDescription(description: string): void;
70
+ isDialogCompletelyOffScreen(): boolean;
71
+ isSurfaceClipped(): boolean;
66
72
  private _forceClose;
67
73
  }
@@ -6,6 +6,7 @@
6
6
  import { getShadowElement, playKeyframeAnimation } from '@tylertech/forge-core';
7
7
  import { BACKDROP_CONSTANTS } from '../backdrop';
8
8
  import { BaseAdapter } from '../core/base/base-adapter';
9
+ import { isElementClipped, moveElementIntoViewport } from '../core/utils/utils';
9
10
  import { DialogComponent } from './dialog';
10
11
  import { DIALOG_CONSTANTS, dialogStack, hideBackdrop, showBackdrop } from './dialog-constants';
11
12
  export class DialogAdapter extends BaseAdapter {
@@ -133,6 +134,9 @@ export class DialogAdapter extends BaseAdapter {
133
134
  removeSurfaceClass(className) {
134
135
  this._surfaceElement.classList.remove(className);
135
136
  }
137
+ moveSurfaceIntoView() {
138
+ moveElementIntoViewport(this._surfaceElement);
139
+ }
136
140
  addFullscreenListener(breakpoint, listener) {
137
141
  this._fullscreenMediaQuery = window.matchMedia(`(max-width: ${breakpoint}px)`);
138
142
  this._fullscreenMediaQuery.addEventListener('change', event => listener(event.matches));
@@ -150,6 +154,18 @@ export class DialogAdapter extends BaseAdapter {
150
154
  setAccessibleDescription(description) {
151
155
  this._accessibleDescriptionElement.textContent = description;
152
156
  }
157
+ isDialogCompletelyOffScreen() {
158
+ if (!this._surfaceElement) {
159
+ return false;
160
+ }
161
+ const rect = this._surfaceElement.getBoundingClientRect();
162
+ const viewportWidth = window.innerWidth;
163
+ const viewportHeight = window.innerHeight;
164
+ return rect.right <= 0 || rect.left >= viewportWidth || rect.bottom <= 0 || rect.top >= viewportHeight;
165
+ }
166
+ isSurfaceClipped() {
167
+ return isElementClipped(this._surfaceElement);
168
+ }
153
169
  _forceClose() {
154
170
  this._surfaceElement.classList.remove(BACKDROP_CONSTANTS.classes.EXITING);
155
171
  this._dialogElement.close();
@@ -173,6 +173,10 @@ export class DialogCore {
173
173
  return event.defaultPrevented;
174
174
  };
175
175
  const onMoveEnd = () => {
176
+ // Move dialog back into view if the surface is clipped
177
+ if (this._adapter.isSurfaceClipped()) {
178
+ this._adapter.moveSurfaceIntoView();
179
+ }
176
180
  const event = new CustomEvent(DIALOG_CONSTANTS.events.MOVE_END);
177
181
  this._adapter.removeSurfaceClass(DIALOG_CONSTANTS.classes.MOVING);
178
182
  this._adapter.dispatchHostEvent(event);
@@ -100,7 +100,7 @@ export class ExpansionPanelAdapter extends BaseAdapter {
100
100
  this._triggerElement?.removeAttribute('aria-expanded');
101
101
  }
102
102
  _getTriggerElementById(id) {
103
- if (id) {
103
+ if (id && this.isConnected) {
104
104
  const rootNode = this._component.getRootNode();
105
105
  return rootNode.getElementById(id);
106
106
  }
@@ -148,10 +148,8 @@ export class ExpansionPanelCore {
148
148
  if (this._trigger !== value) {
149
149
  this._clearTrigger();
150
150
  this._trigger = value;
151
- if (this._adapter.isConnected) {
152
- this._adapter.setTriggerElementById(this._trigger);
153
- this._syncTrigger();
154
- }
151
+ this._adapter.setTriggerElementById(this._trigger);
152
+ this._syncTrigger();
155
153
  }
156
154
  }
157
155
  get triggerElement() {
@@ -161,9 +159,7 @@ export class ExpansionPanelCore {
161
159
  if (this._adapter.triggerElement !== el) {
162
160
  this._clearTrigger();
163
161
  this._adapter.setTriggerElement(el);
164
- if (this._adapter.isConnected) {
165
- this._syncTrigger();
166
- }
162
+ this._syncTrigger();
167
163
  }
168
164
  }
169
165
  }
@@ -378,10 +378,10 @@ export class MenuCore extends CascadingListDropdownAwareCore {
378
378
  _createCascadingElement({ index, options, parentValue }) {
379
379
  const menu = this._adapter.createChildMenu(index, parentValue, this._onCascadingChildOpen.bind(this), this._onCascadingChildClose.bind(this), this._onCascadingOptionSelected.bind(this));
380
380
  menu.mode = 'cascade';
381
- menu.popupOffset = { mainAxis: 0, crossAxis: -8 };
381
+ menu.popupOffset = { alignmentAxis: -8 };
382
382
  menu.dense = this._dense;
383
383
  menu.placement = 'right-start';
384
- menu.fallbackPlacements = ['left-start', 'right-start', 'bottom', 'top'];
384
+ menu.fallbackPlacements = ['left-start', 'left-end', 'right-start', 'right-end', 'bottom', 'top'];
385
385
  menu.persistSelection = this._persistSelection;
386
386
  if (this._persistSelection) {
387
387
  menu.selectedValue = this._selectedValue;
@@ -14,7 +14,7 @@ import { IconComponent, IconRegistry } from '../../icon';
14
14
  import { StateLayerComponent } from '../../state-layer';
15
15
  import { FocusIndicatorComponent } from '../../focus-indicator';
16
16
  const template = '<template><div class=\"forge-split-view-panel\" id=\"root\" part=\"root\"><div class=\"forge-split-view-panel__handle\" id=\"handle\" part=\"handle\" role=\"separator\" aria-controls=\"content\" aria-grabbed=\"false\" tabindex=\"0\"><forge-icon class=\"forge-split-view-panel__icon\" id=\"icon\" part=\"icon\"></forge-icon><forge-state-layer target=\"handle\" id=\"state-layer\" exportparts=\"surface:state-layer\"></forge-state-layer><forge-focus-indicator inward target=\"handle\" part=\"focus-indicator\"></forge-focus-indicator></div><div class=\"forge-split-view-panel__content\" id=\"content\" part=\"content\" role=\"group\"><slot></slot></div></div></template>';
17
- const styles = '.forge-split-view-panel{display:flex;width:100%;height:100%;overflow:hidden}.forge-split-view-panel__handle{color:var(--forge-theme-text-medium,rgba(0,0,0,.6));background-color:var(--forge-theme-outline,#e0e0e0);position:relative;display:flex;flex-shrink:0;justify-content:center;align-items:center;outline:0}.forge-split-view-panel__content{flex:1;overflow:hidden}.forge-split-view-panel--closed{display:none}.forge-split-view-panel--disabled #handle{pointer-events:none}.forge-split-view-panel--disabled .forge-split-view-panel__icon{display:none}.forge-split-view-panel[orientation=horizontal]{min-width:var(--forge-split-view-handle-width,8px);width:calc(var(--forge-split-view-panel-size,unset) + var(--forge-split-view-handle-width,8px));flex-direction:row}.forge-split-view-panel[orientation=horizontal] .forge-split-view-panel__handle{width:var(--forge-split-view-handle-width,8px);cursor:var(--forge-split-view-panel-cursor)}.forge-split-view-panel[orientation=horizontal].forge-split-view-panel--closing[resizable=end]{position:absolute;top:0;left:0;animation-name:uqqok2l;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1))}@keyframes uqqok2l{from{transform:none}to{transform:translateX(-100%)}}.forge-split-view-panel[orientation=horizontal].forge-split-view-panel--closing[resizable=start]{position:absolute;top:0;right:0;animation-name:uqqok3h;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1))}@keyframes uqqok3h{from{transform:none}to{transform:translateX(100%)}}.forge-split-view-panel[orientation=horizontal].forge-split-view-panel--opening[resizable=end]{position:absolute;top:0;left:0;animation-name:uqqok46;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1));animation-direction:reverse}@keyframes uqqok46{from{transform:none}to{transform:translateX(-100%)}}.forge-split-view-panel[orientation=horizontal].forge-split-view-panel--opening[resizable=start]{position:absolute;top:0;right:0;animation-name:uqqok4t;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1));animation-direction:reverse}@keyframes uqqok4t{from{transform:none}to{transform:translateX(100%)}}.forge-split-view-panel[orientation=vertical]{min-height:var(--forge-split-view-handle-width,8px);height:calc(var(--forge-split-view-panel-size,unset) + var(--forge-split-view-handle-width,8px));flex-direction:column}.forge-split-view-panel[orientation=vertical] .forge-split-view-panel__handle{height:var(--forge-split-view-handle-width,8px);cursor:var(--forge-split-view-panel-cursor)}.forge-split-view-panel[orientation=vertical].forge-split-view-panel--closing[resizable=end]{position:absolute;top:0;left:0;animation-name:uqqok5m;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1))}@keyframes uqqok5m{from{transform:none}to{transform:translateY(-100%)}}.forge-split-view-panel[orientation=vertical].forge-split-view-panel--closing[resizable=start]{position:absolute;bottom:0;left:0;animation-name:uqqok6i;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1))}@keyframes uqqok6i{from{transform:none}to{transform:translateY(100%)}}.forge-split-view-panel[orientation=vertical].forge-split-view-panel--opening[resizable=end]{position:absolute;top:0;left:0;animation-name:uqqok6t;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1));animation-direction:reverse}@keyframes uqqok6t{from{transform:none}to{transform:translateY(-100%)}}.forge-split-view-panel[orientation=vertical].forge-split-view-panel--opening[resizable=start]{position:absolute;bottom:0;left:0;animation-name:uqqok7f;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1));animation-direction:reverse}@keyframes uqqok7f{from{transform:none}to{transform:translateY(100%)}}:host{z-index:var(--forge-split-view-animating-layer)!important;display:block;position:relative;height:100%;width:100%;flex:0}:host([hidden]){display:none}:host(:not([resizable=start],[resizable=end])){flex:1}:host(:not([resizable=start],[resizable=end])) .forge-split-view-panel{width:100%;height:100%;min-width:0;min-height:0}:host(:not([resizable=start],[resizable=end])) .forge-split-view-panel__handle{display:none}forge-focus-indicator{--forge-focus-indicator-active-width:2px}';
17
+ const styles = '.forge-split-view-panel{display:flex;width:100%;height:100%;overflow:hidden}.forge-split-view-panel__handle{color:var(--forge-theme-text-medium,rgba(0,0,0,.6));background-color:var(--forge-theme-outline,#e0e0e0);position:relative;display:flex;flex-shrink:0;justify-content:center;align-items:center;outline:0}.forge-split-view-panel__content{flex:1;overflow:hidden}.forge-split-view-panel--closed{display:none}.forge-split-view-panel--disabled #handle{pointer-events:none}.forge-split-view-panel--disabled .forge-split-view-panel__icon{display:none}.forge-split-view-panel[orientation=horizontal]{min-width:var(--forge-split-view-handle-width,8px);width:calc(var(--forge-split-view-panel-size,unset) + var(--forge-split-view-handle-width,8px));flex-direction:row}.forge-split-view-panel[orientation=horizontal] .forge-split-view-panel__handle{width:var(--forge-split-view-handle-width,8px);cursor:var(--forge-split-view-panel-cursor)}.forge-split-view-panel[orientation=horizontal].forge-split-view-panel--closing[resizable=end]{position:absolute;top:0;left:0;animation-name:uhqbsid;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1))}@keyframes uhqbsid{from{transform:none}to{transform:translateX(-100%)}}.forge-split-view-panel[orientation=horizontal].forge-split-view-panel--closing[resizable=start]{position:absolute;top:0;right:0;animation-name:uhqbsj9;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1))}@keyframes uhqbsj9{from{transform:none}to{transform:translateX(100%)}}.forge-split-view-panel[orientation=horizontal].forge-split-view-panel--opening[resizable=end]{position:absolute;top:0;left:0;animation-name:uhqbsjp;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1));animation-direction:reverse}@keyframes uhqbsjp{from{transform:none}to{transform:translateX(-100%)}}.forge-split-view-panel[orientation=horizontal].forge-split-view-panel--opening[resizable=start]{position:absolute;top:0;right:0;animation-name:uhqbskm;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1));animation-direction:reverse}@keyframes uhqbskm{from{transform:none}to{transform:translateX(100%)}}.forge-split-view-panel[orientation=vertical]{min-height:var(--forge-split-view-handle-width,8px);height:calc(var(--forge-split-view-panel-size,unset) + var(--forge-split-view-handle-width,8px));flex-direction:column}.forge-split-view-panel[orientation=vertical] .forge-split-view-panel__handle{height:var(--forge-split-view-handle-width,8px);cursor:var(--forge-split-view-panel-cursor)}.forge-split-view-panel[orientation=vertical].forge-split-view-panel--closing[resizable=end]{position:absolute;top:0;left:0;animation-name:uhqbslj;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1))}@keyframes uhqbslj{from{transform:none}to{transform:translateY(-100%)}}.forge-split-view-panel[orientation=vertical].forge-split-view-panel--closing[resizable=start]{position:absolute;bottom:0;left:0;animation-name:uhqbslt;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1))}@keyframes uhqbslt{from{transform:none}to{transform:translateY(100%)}}.forge-split-view-panel[orientation=vertical].forge-split-view-panel--opening[resizable=end]{position:absolute;top:0;left:0;animation-name:uhqbsm4;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1));animation-direction:reverse}@keyframes uhqbsm4{from{transform:none}to{transform:translateY(-100%)}}.forge-split-view-panel[orientation=vertical].forge-split-view-panel--opening[resizable=start]{position:absolute;bottom:0;left:0;animation-name:uhqbsmg;animation-duration:var(--forge-animation-duration-medium2, 300ms);animation-timing-function:var(--forge-animation-easing-standard,cubic-bezier(0.2,0,0,1));animation-direction:reverse}@keyframes uhqbsmg{from{transform:none}to{transform:translateY(100%)}}:host{z-index:var(--forge-split-view-animating-layer)!important;display:block;position:relative;height:100%;width:100%;flex:0}:host([hidden]){display:none}:host(:not([resizable=start],[resizable=end])){flex:1}:host(:not([resizable=start],[resizable=end])) .forge-split-view-panel{width:100%;height:100%;min-width:0;min-height:0}:host(:not([resizable=start],[resizable=end])) .forge-split-view-panel__handle{display:none}forge-focus-indicator{--forge-focus-indicator-active-width:2px}';
18
18
  /**
19
19
  * @tag forge-split-view-panel
20
20
  *
@@ -76,6 +76,7 @@ export class ToastCore {
76
76
  return this._duration;
77
77
  }
78
78
  set duration(value) {
79
+ value ?? (value = TOAST_CONSTANTS.defaults.DURATION);
79
80
  if (this._duration !== value) {
80
81
  this._duration = value;
81
82
  if (this._hideTimeout) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tylertech/forge",
3
3
  "description": "Tyler Forge™ Web Components library",
4
- "version": "3.10.4",
4
+ "version": "3.10.5",
5
5
  "author": "Tyler Technologies, Inc.",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {