@ship-ui/core 0.17.15 → 0.17.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.
@@ -1062,9 +1062,15 @@
1062
1062
  "outputs": [],
1063
1063
  "methods": [
1064
1064
  {
1065
- "name": "cancelAnimationFrame",
1066
- "parameters": "this.rafId);\n }\n this.rafId = requestAnimationFrame(this.calculateTooltipPosition);\n };\n\n private calculateTooltipPosition = (",
1067
- "returnType": "void =>",
1065
+ "name": "while",
1066
+ "parameters": "parent",
1067
+ "returnType": "any",
1068
+ "description": ""
1069
+ },
1070
+ {
1071
+ "name": "return",
1072
+ "parameters": "pos.left >= 0 &&\n pos.top >= 0 &&\n pos.left + m.width <= window.innerWidth &&\n pos.top + m.height <= window.innerHeight\n );\n }\n\n #clampToViewport(pos: { left: number; top: number }, m: DOMRect",
1073
+ "returnType": "any",
1068
1074
  "description": ""
1069
1075
  },
1070
1076
  {
@@ -3258,7 +3264,7 @@
3258
3264
  },
3259
3265
  {
3260
3266
  "name": "search-menu-example",
3261
- "html": "<sh-menu [searchable]=\"true\">\n <button shButton class=\"outlined\">Open searchable menu</button>\n <ng-container menu>\n @for (item of filteredItems; track item.value) {\n <button (click)=\"select(item)\" [class.selected]=\"selected === item.value\">\n <p>\n hello world\n <br />\n {{ item.label }}\n </p>\n </button>\n }\n </ng-container>\n</sh-menu>\n",
3267
+ "html": "<sh-menu [searchable]=\"true\">\n <button shButton class=\"outlined\">Open searchable menu</button>\n <ng-container menu>\n @for (item of filteredItems; track item.value) {\n <button (click)=\"select(item)\" [class.selected]=\"selected === item.value\">\n <p>\n hello world\n <br />\n {{ item.label }}\n </p>\n </button>\n }\n </ng-container>\n</sh-menu>\n\n<sh-menu [searchable]=\"true\">\n <button shButton class=\"outlined\">Open searchable menu</button>\n <ng-container menu>\n @for (item of filteredItems; track item.value) {\n <button (click)=\"select(item)\" [class.selected]=\"selected === item.value\">\n <div class=\"option-col\">\n {{ item.label }} asdlkjadskljjkladsjkldaljkdaslkjad jklsjkl dasjkld as\n <p>hello world but im extra long so i should wrap to the next line</p>\n </div>\n </button>\n }\n </ng-container>\n</sh-menu>\n",
3262
3268
  "ts": "import { Component } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { ShipButton, ShipMenu } from 'ship-ui';\n\n@Component({\n selector: 'sh-search-menu-example',\n templateUrl: './search-menu-example.html',\n styleUrls: ['./search-menu-example.scss'],\n imports: [FormsModule, ShipMenu, ShipButton],\n standalone: true,\n})\nexport class SearchMenuExample {\n menuItems = [\n { label: 'Dashboard', value: 'dashboard' },\n { label: 'Users', value: 'users' },\n { label: 'Settings', value: 'settings' },\n { label: 'Billing', value: 'billing' },\n { label: 'Support', value: 'support' },\n ];\n search = '';\n selected: string | null = null;\n\n get filteredItems() {\n return this.menuItems.filter((item) => item.label.toLowerCase().includes(this.search.toLowerCase()));\n }\n\n select(item: any) {\n this.selected = item.value;\n }\n}\n"
3263
3269
  },
3264
3270
  {
@@ -7014,31 +7014,56 @@ class ShipTooltipWrapper {
7014
7014
  close: () => this.close()(),
7015
7015
  };
7016
7016
  this.isTemplate = computed(() => this.content() instanceof TemplateRef, ...(ngDevMode ? [{ debugName: "isTemplate" }] : []));
7017
+ this.#document = inject(DOCUMENT);
7017
7018
  this.#selfRef = inject((ElementRef));
7018
7019
  this.#renderer = inject(Renderer2);
7019
7020
  this.#positionAbort = null;
7020
- this.SUPPORTS_ANCHOR = typeof CSS !== 'undefined' && CSS.supports('position-anchor', '--abc');
7021
+ this.SUPPORTS_ANCHOR = typeof CSS !== 'undefined' && CSS.supports('position-anchor', '--abc') && CSS.supports('anchor-name', '--abc');
7021
7022
  this.isBelow = signal(false, ...(ngDevMode ? [{ debugName: "isBelow" }] : []));
7022
7023
  this.openEffect = effect(() => {
7023
7024
  if (this.isOpen()) {
7024
- setTimeout(() => {
7025
- this.#selfRef.nativeElement.showPopover();
7026
- this.schedulePositionUpdate();
7025
+ queueMicrotask(() => {
7026
+ const tooltipEl = this.#selfRef.nativeElement;
7027
+ if (!tooltipEl || !tooltipEl.isConnected)
7028
+ return;
7029
+ if (this.#positionAbort) {
7030
+ this.#positionAbort.abort();
7031
+ }
7032
+ this.#positionAbort = new AbortController();
7033
+ tooltipEl.showPopover();
7034
+ if (!this.SUPPORTS_ANCHOR) {
7035
+ setTimeout(() => {
7036
+ const scrollableParent = this.#findScrollableParent(tooltipEl);
7037
+ scrollableParent.addEventListener('scroll', () => this.calculateTooltipPosition(), {
7038
+ signal: this.#positionAbort?.signal,
7039
+ passive: true,
7040
+ });
7041
+ window?.addEventListener('resize', () => this.calculateTooltipPosition(), {
7042
+ signal: this.#positionAbort?.signal,
7043
+ passive: true,
7044
+ });
7045
+ this.calculateTooltipPosition();
7046
+ });
7047
+ }
7027
7048
  });
7028
7049
  }
7029
7050
  else {
7030
- this.#selfRef.nativeElement.hidePopover();
7051
+ const tooltipEl = this.#selfRef.nativeElement;
7052
+ if (tooltipEl) {
7053
+ try {
7054
+ if (tooltipEl.matches(':popover-open')) {
7055
+ tooltipEl.hidePopover();
7056
+ }
7057
+ }
7058
+ catch (e) {
7059
+ // Ignore if already hidden or other errors
7060
+ }
7061
+ }
7062
+ this.#positionAbort?.abort();
7063
+ this.#positionAbort = null;
7031
7064
  }
7032
7065
  }, ...(ngDevMode ? [{ debugName: "openEffect" }] : []));
7033
- this.rafId = null;
7034
- this.schedulePositionUpdate = () => {
7035
- if (this.rafId !== null) {
7036
- cancelAnimationFrame(this.rafId);
7037
- }
7038
- this.rafId = requestAnimationFrame(this.calculateTooltipPosition);
7039
- };
7040
7066
  this.calculateTooltipPosition = () => {
7041
- this.rafId = null;
7042
7067
  if (!this.anchorEl())
7043
7068
  return;
7044
7069
  const hostRect = this.anchorEl().nativeElement.getBoundingClientRect();
@@ -7046,56 +7071,71 @@ class ShipTooltipWrapper {
7046
7071
  const tooltipRect = tooltipEl.getBoundingClientRect();
7047
7072
  if (tooltipRect.width === 0 && tooltipRect.height === 0)
7048
7073
  return;
7049
- const outOfBoundsTop = hostRect.top - tooltipRect.height < 0;
7050
- if (this.isBelow() !== outOfBoundsTop) {
7051
- this.isBelow.set(outOfBoundsTop);
7052
- }
7053
- if (!this.SUPPORTS_ANCHOR) {
7054
- const tooltipRect = tooltipEl.getBoundingClientRect();
7055
- let newTop = hostRect.top - tooltipRect.height;
7056
- let newLeft = hostRect.left + hostRect.width / 2 - tooltipRect.width / 2;
7057
- if (outOfBoundsTop) {
7058
- newTop = hostRect.top + hostRect.height;
7059
- }
7060
- if (newLeft + tooltipRect.width > window?.innerWidth) {
7061
- newLeft = hostRect.right - tooltipRect.width / 2;
7062
- }
7063
- if (newLeft < 0) {
7064
- newLeft = -(tooltipRect.width / 2);
7065
- }
7066
- const leftStyle = `${newLeft}px`;
7067
- const topStyle = `${newTop}px`;
7068
- if (tooltipEl.style.left !== leftStyle) {
7069
- this.#renderer.setStyle(tooltipEl, 'left', leftStyle);
7070
- }
7071
- if (tooltipEl.style.top !== topStyle) {
7072
- this.#renderer.setStyle(tooltipEl, 'top', topStyle);
7073
- }
7074
- if (tooltipEl.style.position !== 'fixed') {
7075
- this.#renderer.setStyle(tooltipEl, 'position', 'fixed');
7074
+ const BASE_SPACE = 8;
7075
+ // Position generators
7076
+ const topCenter = (t, m) => ({
7077
+ left: t.left + t.width / 2 - m.width / 2,
7078
+ top: t.top - m.height - BASE_SPACE,
7079
+ });
7080
+ const bottomCenter = (t, m) => ({
7081
+ left: t.left + t.width / 2 - m.width / 2,
7082
+ top: t.bottom + BASE_SPACE,
7083
+ });
7084
+ const topSpanLeft = (t, m) => ({ left: t.right - m.width, top: t.top - m.height - BASE_SPACE });
7085
+ const topSpanRight = (t, m) => ({ left: t.left, top: t.top - m.height - BASE_SPACE });
7086
+ const bottomSpanLeft = (t, m) => ({ left: t.right - m.width, top: t.bottom + BASE_SPACE });
7087
+ const bottomSpanRight = (t, m) => ({ left: t.left, top: t.bottom + BASE_SPACE });
7088
+ const tryOrder = [topCenter, topSpanLeft, topSpanRight, bottomCenter, bottomSpanLeft, bottomSpanRight];
7089
+ for (const positionFn of tryOrder) {
7090
+ const pos = positionFn(hostRect, tooltipRect);
7091
+ if (this.#fitsInViewport(pos, tooltipRect)) {
7092
+ this.#applyPosition(pos, tooltipEl);
7093
+ this.isBelow.set(pos.top > hostRect.top);
7094
+ return;
7076
7095
  }
7077
7096
  }
7097
+ const fallback = this.#clampToViewport(topCenter(hostRect, tooltipRect), tooltipRect);
7098
+ this.#applyPosition(fallback, tooltipEl);
7099
+ this.isBelow.set(fallback.top > hostRect.top);
7078
7100
  };
7079
7101
  }
7102
+ #document;
7080
7103
  #selfRef;
7081
7104
  #renderer;
7082
7105
  #positionAbort;
7083
- ngAfterViewInit() {
7084
- this.#positionAbort = new AbortController();
7085
- const options = { signal: this.#positionAbort.signal, capture: true, passive: true };
7086
- window?.addEventListener('scroll', this.schedulePositionUpdate, options);
7087
- window?.addEventListener('resize', this.schedulePositionUpdate, {
7088
- signal: this.#positionAbort.signal,
7089
- passive: true,
7090
- });
7091
- this.schedulePositionUpdate();
7092
- }
7093
7106
  ngOnDestroy() {
7094
7107
  this.#positionAbort?.abort();
7095
- if (this.rafId !== null) {
7096
- cancelAnimationFrame(this.rafId);
7097
- this.rafId = null;
7108
+ this.#positionAbort = null;
7109
+ }
7110
+ #findScrollableParent(element) {
7111
+ const SCROLLABLE_STYLES = ['scroll', 'auto'];
7112
+ let parent = element.parentElement;
7113
+ while (parent) {
7114
+ if (SCROLLABLE_STYLES.indexOf(window?.getComputedStyle(parent).overflowY) > -1 &&
7115
+ parent.scrollHeight > parent.clientHeight) {
7116
+ return parent;
7117
+ }
7118
+ parent = parent.parentElement;
7098
7119
  }
7120
+ return this.#document.documentElement;
7121
+ }
7122
+ #applyPosition(pos, element) {
7123
+ this.#renderer.setStyle(element, 'left', `${pos.left}px`);
7124
+ this.#renderer.setStyle(element, 'top', `${pos.top}px`);
7125
+ this.#renderer.setStyle(element, 'position', 'fixed');
7126
+ this.#renderer.setStyle(element, 'margin', '0');
7127
+ }
7128
+ #fitsInViewport(pos, m) {
7129
+ return (pos.left >= 0 &&
7130
+ pos.top >= 0 &&
7131
+ pos.left + m.width <= window.innerWidth &&
7132
+ pos.top + m.height <= window.innerHeight);
7133
+ }
7134
+ #clampToViewport(pos, m) {
7135
+ return {
7136
+ left: Math.max(0, Math.min(pos.left, window.innerWidth - m.width)),
7137
+ top: Math.max(0, Math.min(pos.top, window.innerHeight - m.height)),
7138
+ };
7099
7139
  }
7100
7140
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ShipTooltipWrapper, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
7101
7141
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.0", type: ShipTooltipWrapper, isStandalone: true, selector: "ship-tooltip-wrapper", inputs: { positionAnchorName: { classPropertyName: "positionAnchorName", publicName: "positionAnchorName", isSignal: true, isRequired: true, transformFunction: null }, anchorEl: { classPropertyName: "anchorEl", publicName: "anchorEl", isSignal: true, isRequired: true, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, close: { classPropertyName: "close", publicName: "close", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tooltip" }, properties: { "attr.popover": "\"manual\"", "style.position-anchor": "positionAnchorName()", "class.below": "isBelow()" } }, ngImport: i0, template: `
@@ -7147,7 +7187,7 @@ class ShipTooltip {
7147
7187
  this.#elementRef = inject((ElementRef));
7148
7188
  this.#viewContainerRef = inject(ViewContainerRef);
7149
7189
  this.#environmentInjector = inject(EnvironmentInjector);
7150
- this.#renderer = inject(Renderer2);
7190
+ this.debounceTimer = null;
7151
7191
  this.DEBOUNCE_DELAY = 500;
7152
7192
  this.anchorName = `--${generateUniqueId()}`;
7153
7193
  this.isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
@@ -7156,7 +7196,6 @@ class ShipTooltip {
7156
7196
  #elementRef;
7157
7197
  #viewContainerRef;
7158
7198
  #environmentInjector;
7159
- #renderer;
7160
7199
  onMouseEnter() {
7161
7200
  if (openRef?.component.anchorName !== this.anchorName) {
7162
7201
  this.cleanupTooltip();