cerxos-ui 0.0.8 → 0.0.9

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.
@@ -27,9 +27,11 @@ const TRIGGER_BASE_CLASSES = 'inline-flex items-center justify-center rounded-[v
27
27
  'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ' +
28
28
  'focus-visible:outline-[var(--cxs-color-focus)]';
29
29
  const TRIGGER_DISABLED_CLASSES = 'pointer-events-none opacity-60';
30
- const MENU_BASE_CLASSES = 'absolute z-40 mt-2 min-w-[10rem] rounded-[var(--cxs-radius-md)] ' +
30
+ const MENU_BASE_CLASSES = 'min-w-[10rem] rounded-[var(--cxs-radius-md)] ' +
31
31
  'border border-[var(--cxs-color-border)] bg-[var(--cxs-color-surface)] py-1 ' +
32
32
  'shadow-[var(--cxs-shadow-sm)]';
33
+ const MENU_POSITION_CLASSES = 'absolute z-40 mt-2';
34
+ const MENU_FLOATING_CLASSES = 'fixed z-50';
33
35
  const MENU_ALIGN_CLASSES = {
34
36
  start: 'left-0 origin-top-left',
35
37
  end: 'right-0 origin-top-right'
@@ -49,11 +51,14 @@ class CxsActionMenuComponent {
49
51
  ariaLabel = 'Open actions menu';
50
52
  menuLabel = 'Actions';
51
53
  disabled = false;
54
+ floating = false;
52
55
  itemSelected = new EventEmitter();
53
56
  trigger;
57
+ menu;
54
58
  menuItems;
55
59
  menuId = `cxs-action-menu-${CxsActionMenuComponent.nextId++}`;
56
60
  isOpen = false;
61
+ menuPosition = null;
57
62
  constructor(elementRef) {
58
63
  this.elementRef = elementRef;
59
64
  }
@@ -61,7 +66,13 @@ class CxsActionMenuComponent {
61
66
  return [TRIGGER_BASE_CLASSES, this.disabled ? TRIGGER_DISABLED_CLASSES : ''].filter(Boolean).join(' ');
62
67
  }
63
68
  get menuClass() {
64
- return [MENU_BASE_CLASSES, MENU_ALIGN_CLASSES[this.align]].join(' ');
69
+ return [
70
+ MENU_BASE_CLASSES,
71
+ this.floating ? MENU_FLOATING_CLASSES : MENU_POSITION_CLASSES,
72
+ !this.floating ? MENU_ALIGN_CLASSES[this.align] : ''
73
+ ]
74
+ .filter(Boolean)
75
+ .join(' ');
65
76
  }
66
77
  get emptyStateClass() {
67
78
  return EMPTY_STATE_CLASSES$1;
@@ -91,10 +102,12 @@ class CxsActionMenuComponent {
91
102
  return;
92
103
  }
93
104
  this.isOpen = true;
105
+ this.updateMenuPosition();
94
106
  this.focusFirstItem();
95
107
  }
96
108
  close() {
97
109
  this.isOpen = false;
110
+ this.menuPosition = null;
98
111
  }
99
112
  onTriggerKeydown(event) {
100
113
  if (this.disabled) {
@@ -162,6 +175,12 @@ class CxsActionMenuComponent {
162
175
  this.close();
163
176
  }
164
177
  }
178
+ onWindowResize() {
179
+ if (!this.isOpen) {
180
+ return;
181
+ }
182
+ this.updateMenuPosition();
183
+ }
165
184
  focusFirstItem() {
166
185
  if (typeof document === 'undefined') {
167
186
  return;
@@ -174,17 +193,57 @@ class CxsActionMenuComponent {
174
193
  focusTrigger() {
175
194
  this.trigger?.nativeElement.focus();
176
195
  }
196
+ updateMenuPosition() {
197
+ if (!this.floating) {
198
+ return;
199
+ }
200
+ if (typeof window === 'undefined') {
201
+ return;
202
+ }
203
+ const trigger = this.trigger?.nativeElement;
204
+ if (!trigger) {
205
+ return;
206
+ }
207
+ const rect = trigger.getBoundingClientRect();
208
+ const offset = 8;
209
+ this.menuPosition = {
210
+ top: rect.bottom + offset,
211
+ left: rect.left
212
+ };
213
+ setTimeout(() => {
214
+ const menuEl = this.menu?.nativeElement;
215
+ const menuRect = menuEl?.getBoundingClientRect();
216
+ if (!menuRect) {
217
+ return;
218
+ }
219
+ let top = rect.bottom + offset;
220
+ let left = this.align === 'end' ? rect.right - menuRect.width : rect.left;
221
+ if (top + menuRect.height > window.innerHeight) {
222
+ top = rect.top - menuRect.height - offset;
223
+ }
224
+ if (top < offset) {
225
+ top = offset;
226
+ }
227
+ if (left + menuRect.width > window.innerWidth - offset) {
228
+ left = Math.max(offset, window.innerWidth - menuRect.width - offset);
229
+ }
230
+ if (left < offset) {
231
+ left = offset;
232
+ }
233
+ this.menuPosition = { top, left };
234
+ }, 0);
235
+ }
177
236
  getFocusableItems() {
178
237
  return (this.menuItems?.toArray() ?? [])
179
238
  .map((item) => item.nativeElement)
180
239
  .filter((item) => !item.hasAttribute('disabled') && !item.hasAttribute('data-disabled'));
181
240
  }
182
241
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CxsActionMenuComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
183
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: CxsActionMenuComponent, isStandalone: true, selector: "cxs-action-menu", inputs: { items: "items", align: "align", ariaLabel: "ariaLabel", menuLabel: "menuLabel", disabled: "disabled" }, outputs: { itemSelected: "itemSelected" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, read: ElementRef }, { propertyName: "menuItems", predicate: ["menuItem"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"relative inline-flex\">\n <button\n #trigger\n type=\"button\"\n [class]=\"triggerClass\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"menuId\"\n [attr.aria-label]=\"ariaLabel\"\n (click)=\"toggle()\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <svg class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <circle cx=\"4\" cy=\"10\" r=\"1.5\"></circle>\n <circle cx=\"10\" cy=\"10\" r=\"1.5\"></circle>\n <circle cx=\"16\" cy=\"10\" r=\"1.5\"></circle>\n </svg>\n </button>\n\n @if (isOpen) {\n <div\n [class]=\"menuClass\"\n [attr.id]=\"menuId\"\n role=\"menu\"\n [attr.aria-label]=\"menuLabel\"\n (keydown)=\"onMenuKeydown($event)\"\n >\n @if (items.length) {\n @for (item of items; track item.value) {\n <button\n #menuItem\n type=\"button\"\n role=\"menuitem\"\n [class]=\"getItemClass(item)\"\n [attr.data-disabled]=\"isItemDisabled(item) ? '' : null\"\n [attr.aria-disabled]=\"isItemDisabled(item) ? 'true' : null\"\n [disabled]=\"isItemDisabled(item)\"\n (click)=\"onItemClick(item)\"\n >\n {{ item.label }}\n </button>\n }\n } @else {\n <span [class]=\"emptyStateClass\">No actions</span>\n }\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
242
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: CxsActionMenuComponent, isStandalone: true, selector: "cxs-action-menu", inputs: { items: "items", align: "align", ariaLabel: "ariaLabel", menuLabel: "menuLabel", disabled: "disabled", floating: "floating" }, outputs: { itemSelected: "itemSelected" }, host: { listeners: { "document:click": "onDocumentClick($event)", "window:resize": "onWindowResize()" } }, viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, read: ElementRef }, { propertyName: "menu", first: true, predicate: ["menu"], descendants: true, read: ElementRef }, { propertyName: "menuItems", predicate: ["menuItem"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"relative inline-flex\">\n <button\n #trigger\n type=\"button\"\n [class]=\"triggerClass\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"menuId\"\n [attr.aria-label]=\"ariaLabel\"\n (click)=\"toggle()\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <svg class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <circle cx=\"4\" cy=\"10\" r=\"1.5\"></circle>\n <circle cx=\"10\" cy=\"10\" r=\"1.5\"></circle>\n <circle cx=\"16\" cy=\"10\" r=\"1.5\"></circle>\n </svg>\n </button>\n\n @if (isOpen) {\n <div\n #menu\n [class]=\"menuClass\"\n [attr.id]=\"menuId\"\n role=\"menu\"\n [attr.aria-label]=\"menuLabel\"\n [style.top.px]=\"menuPosition?.top\"\n [style.left.px]=\"menuPosition?.left\"\n (keydown)=\"onMenuKeydown($event)\"\n >\n @if (items.length) {\n @for (item of items; track item.value) {\n <button\n #menuItem\n type=\"button\"\n role=\"menuitem\"\n [class]=\"getItemClass(item)\"\n [attr.data-disabled]=\"isItemDisabled(item) ? '' : null\"\n [attr.aria-disabled]=\"isItemDisabled(item) ? 'true' : null\"\n [disabled]=\"isItemDisabled(item)\"\n (click)=\"onItemClick(item)\"\n >\n {{ item.label }}\n </button>\n }\n } @else {\n <span [class]=\"emptyStateClass\">No actions</span>\n }\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
184
243
  }
185
244
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CxsActionMenuComponent, decorators: [{
186
245
  type: Component,
187
- args: [{ selector: 'cxs-action-menu', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"relative inline-flex\">\n <button\n #trigger\n type=\"button\"\n [class]=\"triggerClass\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"menuId\"\n [attr.aria-label]=\"ariaLabel\"\n (click)=\"toggle()\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <svg class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <circle cx=\"4\" cy=\"10\" r=\"1.5\"></circle>\n <circle cx=\"10\" cy=\"10\" r=\"1.5\"></circle>\n <circle cx=\"16\" cy=\"10\" r=\"1.5\"></circle>\n </svg>\n </button>\n\n @if (isOpen) {\n <div\n [class]=\"menuClass\"\n [attr.id]=\"menuId\"\n role=\"menu\"\n [attr.aria-label]=\"menuLabel\"\n (keydown)=\"onMenuKeydown($event)\"\n >\n @if (items.length) {\n @for (item of items; track item.value) {\n <button\n #menuItem\n type=\"button\"\n role=\"menuitem\"\n [class]=\"getItemClass(item)\"\n [attr.data-disabled]=\"isItemDisabled(item) ? '' : null\"\n [attr.aria-disabled]=\"isItemDisabled(item) ? 'true' : null\"\n [disabled]=\"isItemDisabled(item)\"\n (click)=\"onItemClick(item)\"\n >\n {{ item.label }}\n </button>\n }\n } @else {\n <span [class]=\"emptyStateClass\">No actions</span>\n }\n </div>\n }\n</div>\n" }]
246
+ args: [{ selector: 'cxs-action-menu', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"relative inline-flex\">\n <button\n #trigger\n type=\"button\"\n [class]=\"triggerClass\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"menuId\"\n [attr.aria-label]=\"ariaLabel\"\n (click)=\"toggle()\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <svg class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <circle cx=\"4\" cy=\"10\" r=\"1.5\"></circle>\n <circle cx=\"10\" cy=\"10\" r=\"1.5\"></circle>\n <circle cx=\"16\" cy=\"10\" r=\"1.5\"></circle>\n </svg>\n </button>\n\n @if (isOpen) {\n <div\n #menu\n [class]=\"menuClass\"\n [attr.id]=\"menuId\"\n role=\"menu\"\n [attr.aria-label]=\"menuLabel\"\n [style.top.px]=\"menuPosition?.top\"\n [style.left.px]=\"menuPosition?.left\"\n (keydown)=\"onMenuKeydown($event)\"\n >\n @if (items.length) {\n @for (item of items; track item.value) {\n <button\n #menuItem\n type=\"button\"\n role=\"menuitem\"\n [class]=\"getItemClass(item)\"\n [attr.data-disabled]=\"isItemDisabled(item) ? '' : null\"\n [attr.aria-disabled]=\"isItemDisabled(item) ? 'true' : null\"\n [disabled]=\"isItemDisabled(item)\"\n (click)=\"onItemClick(item)\"\n >\n {{ item.label }}\n </button>\n }\n } @else {\n <span [class]=\"emptyStateClass\">No actions</span>\n }\n </div>\n }\n</div>\n" }]
188
247
  }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { items: [{
189
248
  type: Input
190
249
  }], align: [{
@@ -195,17 +254,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
195
254
  type: Input
196
255
  }], disabled: [{
197
256
  type: Input
257
+ }], floating: [{
258
+ type: Input
198
259
  }], itemSelected: [{
199
260
  type: Output
200
261
  }], trigger: [{
201
262
  type: ViewChild,
202
263
  args: ['trigger', { read: ElementRef }]
264
+ }], menu: [{
265
+ type: ViewChild,
266
+ args: ['menu', { read: ElementRef }]
203
267
  }], menuItems: [{
204
268
  type: ViewChildren,
205
269
  args: ['menuItem', { read: ElementRef }]
206
270
  }], onDocumentClick: [{
207
271
  type: HostListener,
208
272
  args: ['document:click', ['$event']]
273
+ }], onWindowResize: [{
274
+ type: HostListener,
275
+ args: ['window:resize']
209
276
  }] } });
210
277
 
211
278
  const BASE_CLASSES$a = 'relative flex w-full gap-3 rounded-[var(--cxs-radius-md)] border border-l-4 px-4 py-3 text-sm ' +