@shadng/sng-ui 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/cli/sng-ui.js +331 -0
- package/ng-package.json +29 -0
- package/package.json +64 -0
- package/registry.json +72 -0
- package/src/lib/accordion/cn.ts +6 -0
- package/src/lib/accordion/index.ts +18 -0
- package/src/lib/accordion/sng-accordion-content.ts +131 -0
- package/src/lib/accordion/sng-accordion-item.ts +299 -0
- package/src/lib/accordion/sng-accordion-trigger.ts +137 -0
- package/src/lib/accordion/sng-accordion.ts +118 -0
- package/src/lib/accordion/sng-accordion.types.ts +82 -0
- package/src/lib/alert/cn.ts +6 -0
- package/src/lib/alert/index.ts +3 -0
- package/src/lib/alert/sng-alert-description.ts +49 -0
- package/src/lib/alert/sng-alert-title.ts +46 -0
- package/src/lib/alert/sng-alert.ts +48 -0
- package/src/lib/avatar/cn.ts +6 -0
- package/src/lib/avatar/index.ts +3 -0
- package/src/lib/avatar/sng-avatar-fallback.ts +50 -0
- package/src/lib/avatar/sng-avatar-image.ts +73 -0
- package/src/lib/avatar/sng-avatar.ts +60 -0
- package/src/lib/badge/cn.ts +6 -0
- package/src/lib/badge/index.ts +1 -0
- package/src/lib/badge/sng-badge.ts +36 -0
- package/src/lib/breadcrumb/cn.ts +6 -0
- package/src/lib/breadcrumb/index.ts +7 -0
- package/src/lib/breadcrumb/sng-breadcrumb-ellipsis.ts +61 -0
- package/src/lib/breadcrumb/sng-breadcrumb-item.ts +47 -0
- package/src/lib/breadcrumb/sng-breadcrumb-link.ts +43 -0
- package/src/lib/breadcrumb/sng-breadcrumb-list.ts +42 -0
- package/src/lib/breadcrumb/sng-breadcrumb-page.ts +44 -0
- package/src/lib/breadcrumb/sng-breadcrumb-separator.ts +60 -0
- package/src/lib/breadcrumb/sng-breadcrumb.ts +52 -0
- package/src/lib/button/cn.ts +6 -0
- package/src/lib/button/index.ts +2 -0
- package/src/lib/button/sng-button.ts +264 -0
- package/src/lib/calendar/cn.ts +6 -0
- package/src/lib/calendar/index.ts +2 -0
- package/src/lib/calendar/sng-calendar.ts +753 -0
- package/src/lib/card/cn.ts +6 -0
- package/src/lib/card/index.ts +6 -0
- package/src/lib/card/sng-card-content.ts +36 -0
- package/src/lib/card/sng-card-description.ts +38 -0
- package/src/lib/card/sng-card-footer.ts +34 -0
- package/src/lib/card/sng-card-header.ts +34 -0
- package/src/lib/card/sng-card-title.ts +48 -0
- package/src/lib/card/sng-card.ts +43 -0
- package/src/lib/carousel/cn.ts +6 -0
- package/src/lib/carousel/index.ts +18 -0
- package/src/lib/carousel/sng-carousel.ts +526 -0
- package/src/lib/checkbox/cn.ts +6 -0
- package/src/lib/checkbox/index.ts +1 -0
- package/src/lib/checkbox/sng-checkbox.ts +154 -0
- package/src/lib/code-block/cn.ts +6 -0
- package/src/lib/code-block/index.ts +1 -0
- package/src/lib/code-block/sng-code-block.ts +296 -0
- package/src/lib/dialog/cn.ts +6 -0
- package/src/lib/dialog/index.ts +37 -0
- package/src/lib/dialog/sng-dialog-close.ts +76 -0
- package/src/lib/dialog/sng-dialog-content.ts +132 -0
- package/src/lib/dialog/sng-dialog-description.ts +36 -0
- package/src/lib/dialog/sng-dialog-footer.ts +39 -0
- package/src/lib/dialog/sng-dialog-header.ts +39 -0
- package/src/lib/dialog/sng-dialog-title.ts +52 -0
- package/src/lib/dialog/sng-dialog.service.ts +222 -0
- package/src/lib/dialog/sng-dialog.ts +224 -0
- package/src/lib/drawer/cn.ts +6 -0
- package/src/lib/drawer/index.ts +36 -0
- package/src/lib/drawer/sng-drawer-close.ts +28 -0
- package/src/lib/drawer/sng-drawer-content.ts +135 -0
- package/src/lib/drawer/sng-drawer-description.ts +29 -0
- package/src/lib/drawer/sng-drawer-footer.ts +34 -0
- package/src/lib/drawer/sng-drawer-handle.ts +30 -0
- package/src/lib/drawer/sng-drawer-header.ts +30 -0
- package/src/lib/drawer/sng-drawer-title.ts +27 -0
- package/src/lib/drawer/sng-drawer-trigger.ts +21 -0
- package/src/lib/drawer/sng-drawer-wrapper.ts +27 -0
- package/src/lib/drawer/sng-drawer.ts +166 -0
- package/src/lib/file-input/cn.ts +6 -0
- package/src/lib/file-input/index.ts +1 -0
- package/src/lib/file-input/sng-file-input.ts +288 -0
- package/src/lib/hover-card/cn.ts +6 -0
- package/src/lib/hover-card/index.ts +3 -0
- package/src/lib/hover-card/sng-hover-card-content.ts +100 -0
- package/src/lib/hover-card/sng-hover-card-trigger.ts +43 -0
- package/src/lib/hover-card/sng-hover-card.ts +246 -0
- package/src/lib/input/cn.ts +6 -0
- package/src/lib/input/index.ts +1 -0
- package/src/lib/input/sng-input.ts +160 -0
- package/src/lib/layout/cn.ts +6 -0
- package/src/lib/layout/index.ts +98 -0
- package/src/lib/layout/sng-layout-footer.ts +37 -0
- package/src/lib/layout/sng-layout-header.ts +38 -0
- package/src/lib/layout/sng-layout-sidebar-content.ts +149 -0
- package/src/lib/layout/sng-layout-sidebar-footer.ts +54 -0
- package/src/lib/layout/sng-layout-sidebar-group-action.ts +67 -0
- package/src/lib/layout/sng-layout-sidebar-group-content.ts +41 -0
- package/src/lib/layout/sng-layout-sidebar-group-label.ts +53 -0
- package/src/lib/layout/sng-layout-sidebar-group.ts +41 -0
- package/src/lib/layout/sng-layout-sidebar-header.ts +54 -0
- package/src/lib/layout/sng-layout-sidebar-input.ts +112 -0
- package/src/lib/layout/sng-layout-sidebar-inset.ts +45 -0
- package/src/lib/layout/sng-layout-sidebar-menu-action.ts +84 -0
- package/src/lib/layout/sng-layout-sidebar-menu-badge.ts +47 -0
- package/src/lib/layout/sng-layout-sidebar-menu-button.ts +160 -0
- package/src/lib/layout/sng-layout-sidebar-menu-item.ts +40 -0
- package/src/lib/layout/sng-layout-sidebar-menu-skeleton.ts +71 -0
- package/src/lib/layout/sng-layout-sidebar-menu-sub-button.ts +142 -0
- package/src/lib/layout/sng-layout-sidebar-menu-sub-item.ts +38 -0
- package/src/lib/layout/sng-layout-sidebar-menu-sub.ts +48 -0
- package/src/lib/layout/sng-layout-sidebar-menu.ts +41 -0
- package/src/lib/layout/sng-layout-sidebar-provider.ts +189 -0
- package/src/lib/layout/sng-layout-sidebar-rail.ts +60 -0
- package/src/lib/layout/sng-layout-sidebar-separator.ts +38 -0
- package/src/lib/layout/sng-layout-sidebar-trigger.ts +97 -0
- package/src/lib/layout/sng-layout-sidebar.ts +254 -0
- package/src/lib/menu/cn.ts +6 -0
- package/src/lib/menu/index.ts +21 -0
- package/src/lib/menu/sng-context-trigger.ts +128 -0
- package/src/lib/menu/sng-menu-checkbox-item.ts +91 -0
- package/src/lib/menu/sng-menu-item.ts +80 -0
- package/src/lib/menu/sng-menu-label.ts +47 -0
- package/src/lib/menu/sng-menu-radio-group.ts +38 -0
- package/src/lib/menu/sng-menu-radio-item.ts +94 -0
- package/src/lib/menu/sng-menu-separator.ts +27 -0
- package/src/lib/menu/sng-menu-shortcut.ts +25 -0
- package/src/lib/menu/sng-menu-sub-content.ts +267 -0
- package/src/lib/menu/sng-menu-sub-trigger.ts +68 -0
- package/src/lib/menu/sng-menu-sub.ts +124 -0
- package/src/lib/menu/sng-menu-tokens.ts +52 -0
- package/src/lib/menu/sng-menu-trigger.ts +266 -0
- package/src/lib/menu/sng-menu.ts +100 -0
- package/src/lib/nav-menu/cn.ts +6 -0
- package/src/lib/nav-menu/index.ts +6 -0
- package/src/lib/nav-menu/sng-nav-menu-content.ts +72 -0
- package/src/lib/nav-menu/sng-nav-menu-item.ts +109 -0
- package/src/lib/nav-menu/sng-nav-menu-link.ts +54 -0
- package/src/lib/nav-menu/sng-nav-menu-list.ts +43 -0
- package/src/lib/nav-menu/sng-nav-menu-trigger.ts +98 -0
- package/src/lib/nav-menu/sng-nav-menu.ts +99 -0
- package/src/lib/otp-input/cn.ts +6 -0
- package/src/lib/otp-input/index.ts +14 -0
- package/src/lib/otp-input/sng-otp-input-group.ts +38 -0
- package/src/lib/otp-input/sng-otp-input-separator.ts +43 -0
- package/src/lib/otp-input/sng-otp-input-slot.ts +128 -0
- package/src/lib/otp-input/sng-otp-input-tokens.ts +20 -0
- package/src/lib/otp-input/sng-otp-input.ts +301 -0
- package/src/lib/popover/cn.ts +6 -0
- package/src/lib/popover/index.ts +3 -0
- package/src/lib/popover/sng-popover-content.ts +66 -0
- package/src/lib/popover/sng-popover-trigger.ts +44 -0
- package/src/lib/popover/sng-popover.ts +218 -0
- package/src/lib/preview-box/cn.ts +6 -0
- package/src/lib/preview-box/index.ts +5 -0
- package/src/lib/preview-box/sng-code-block.ts +80 -0
- package/src/lib/preview-box/sng-html-block.ts +79 -0
- package/src/lib/preview-box/sng-preview-block.ts +47 -0
- package/src/lib/preview-box/sng-preview-box.ts +369 -0
- package/src/lib/preview-box/sng-style-block.ts +80 -0
- package/src/lib/progress/cn.ts +6 -0
- package/src/lib/progress/index.ts +1 -0
- package/src/lib/progress/sng-progress.ts +65 -0
- package/src/lib/radio/cn.ts +6 -0
- package/src/lib/radio/index.ts +5 -0
- package/src/lib/radio/sng-radio-item.ts +100 -0
- package/src/lib/radio/sng-radio.ts +54 -0
- package/src/lib/resizable/cn.ts +6 -0
- package/src/lib/resizable/index.ts +3 -0
- package/src/lib/resizable/sng-resizable-group.ts +188 -0
- package/src/lib/resizable/sng-resizable-handle.ts +236 -0
- package/src/lib/resizable/sng-resizable-panel.ts +71 -0
- package/src/lib/search-input/cn.ts +6 -0
- package/src/lib/search-input/index.ts +16 -0
- package/src/lib/search-input/sng-search-input-context.ts +24 -0
- package/src/lib/search-input/sng-search-input-empty.ts +42 -0
- package/src/lib/search-input/sng-search-input-group.ts +69 -0
- package/src/lib/search-input/sng-search-input-item.ts +164 -0
- package/src/lib/search-input/sng-search-input-list.ts +34 -0
- package/src/lib/search-input/sng-search-input-separator.ts +32 -0
- package/src/lib/search-input/sng-search-input-shortcut.ts +29 -0
- package/src/lib/search-input/sng-search-input.ts +368 -0
- package/src/lib/select/cn.ts +6 -0
- package/src/lib/select/index.ts +7 -0
- package/src/lib/select/sng-select-content.ts +27 -0
- package/src/lib/select/sng-select-empty.ts +48 -0
- package/src/lib/select/sng-select-group.ts +29 -0
- package/src/lib/select/sng-select-item.ts +140 -0
- package/src/lib/select/sng-select-label.ts +29 -0
- package/src/lib/select/sng-select-separator.ts +29 -0
- package/src/lib/select/sng-select.ts +326 -0
- package/src/lib/separator/cn.ts +6 -0
- package/src/lib/separator/index.ts +1 -0
- package/src/lib/separator/sng-separator.ts +40 -0
- package/src/lib/skeleton/cn.ts +6 -0
- package/src/lib/skeleton/index.ts +1 -0
- package/src/lib/skeleton/sng-skeleton.ts +49 -0
- package/src/lib/slider/cn.ts +6 -0
- package/src/lib/slider/index.ts +2 -0
- package/src/lib/slider/sng-slider.ts +137 -0
- package/src/lib/sng-table/cn.ts +6 -0
- package/src/lib/sng-table/flex-render.ts +222 -0
- package/src/lib/sng-table/index.ts +85 -0
- package/src/lib/sng-table/sng-table-body.ts +59 -0
- package/src/lib/sng-table/sng-table-caption.ts +49 -0
- package/src/lib/sng-table/sng-table-cell.ts +62 -0
- package/src/lib/sng-table/sng-table-footer.ts +60 -0
- package/src/lib/sng-table/sng-table-head.ts +66 -0
- package/src/lib/sng-table/sng-table-header.ts +48 -0
- package/src/lib/sng-table/sng-table-pagination.ts +265 -0
- package/src/lib/sng-table/sng-table-row.ts +65 -0
- package/src/lib/sng-table/sng-table.ts +67 -0
- package/src/lib/sng-table-core/core/create-cell.ts +117 -0
- package/src/lib/sng-table-core/core/create-column.ts +266 -0
- package/src/lib/sng-table-core/core/create-header.ts +271 -0
- package/src/lib/sng-table-core/core/create-row.ts +293 -0
- package/src/lib/sng-table-core/core/create-table.ts +534 -0
- package/src/lib/sng-table-core/core/types.ts +1197 -0
- package/src/lib/sng-table-core/core/utils.ts +307 -0
- package/src/lib/sng-table-core/features/column-filtering.ts +376 -0
- package/src/lib/sng-table-core/features/column-ordering.ts +159 -0
- package/src/lib/sng-table-core/features/column-pinning.ts +219 -0
- package/src/lib/sng-table-core/features/column-sizing.ts +268 -0
- package/src/lib/sng-table-core/features/column-visibility.ts +128 -0
- package/src/lib/sng-table-core/features/faceting.ts +279 -0
- package/src/lib/sng-table-core/features/fuzzy-filtering.ts +188 -0
- package/src/lib/sng-table-core/features/global-filtering.ts +128 -0
- package/src/lib/sng-table-core/features/pagination.ts +179 -0
- package/src/lib/sng-table-core/features/row-expanding.ts +181 -0
- package/src/lib/sng-table-core/features/row-grouping.ts +235 -0
- package/src/lib/sng-table-core/features/row-pinning.ts +196 -0
- package/src/lib/sng-table-core/features/row-selection.ts +298 -0
- package/src/lib/sng-table-core/features/sorting.ts +425 -0
- package/src/lib/sng-table-core/features/virtualization.ts +298 -0
- package/src/lib/sng-table-core/index.ts +235 -0
- package/src/lib/sng-table-core/row-models/core-row-model.ts +256 -0
- package/src/lib/sng-table-core/row-models/expanded-row-model.ts +175 -0
- package/src/lib/sng-table-core/row-models/filtered-row-model.ts +307 -0
- package/src/lib/sng-table-core/row-models/grouped-row-model.ts +290 -0
- package/src/lib/sng-table-core/row-models/paginated-row-model.ts +135 -0
- package/src/lib/sng-table-core/row-models/sorted-row-model.ts +197 -0
- package/src/lib/styles/sng-themes.css +164 -0
- package/src/lib/switch/cn.ts +6 -0
- package/src/lib/switch/index.ts +1 -0
- package/src/lib/switch/sng-switch.ts +137 -0
- package/src/lib/tabs/cn.ts +6 -0
- package/src/lib/tabs/index.ts +4 -0
- package/src/lib/tabs/sng-tabs-content.ts +66 -0
- package/src/lib/tabs/sng-tabs-list.ts +55 -0
- package/src/lib/tabs/sng-tabs-trigger.ts +86 -0
- package/src/lib/tabs/sng-tabs.ts +83 -0
- package/src/lib/toast/cn.ts +6 -0
- package/src/lib/toast/index.ts +3 -0
- package/src/lib/toast/sng-toast.service.ts +258 -0
- package/src/lib/toast/sng-toast.ts +101 -0
- package/src/lib/toast/sng-toaster.ts +67 -0
- package/src/lib/toggle/cn.ts +6 -0
- package/src/lib/toggle/index.ts +6 -0
- package/src/lib/toggle/sng-toggle-group-item.ts +89 -0
- package/src/lib/toggle/sng-toggle-group.ts +85 -0
- package/src/lib/toggle/sng-toggle.ts +78 -0
- package/src/lib/toggle-group/index.ts +6 -0
- package/src/lib/tooltip/cn.ts +6 -0
- package/src/lib/tooltip/index.ts +5 -0
- package/src/lib/tooltip/sng-tooltip-content.ts +64 -0
- package/src/lib/tooltip/sng-tooltip.ts +216 -0
- package/src/public-api.ts +207 -0
- package/tsconfig.json +24 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +11 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
input,
|
|
5
|
+
computed,
|
|
6
|
+
booleanAttribute,
|
|
7
|
+
} from '@angular/core';
|
|
8
|
+
import { cn } from './cn';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Non-interactive label for grouping or describing menu sections.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```html
|
|
15
|
+
* <sng-menu>
|
|
16
|
+
* <sng-menu-label>Actions</sng-menu-label>
|
|
17
|
+
* <sng-menu-item>Edit</sng-menu-item>
|
|
18
|
+
* <sng-menu-item>Delete</sng-menu-item>
|
|
19
|
+
* </sng-menu>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
@Component({
|
|
23
|
+
selector: 'sng-menu-label',
|
|
24
|
+
standalone: true,
|
|
25
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
26
|
+
host: {
|
|
27
|
+
'[class]': 'hostClasses()',
|
|
28
|
+
},
|
|
29
|
+
template: `<ng-content />`,
|
|
30
|
+
})
|
|
31
|
+
export class SngMenuLabel {
|
|
32
|
+
/** Custom CSS classes. */
|
|
33
|
+
class = input<string>('');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether to add left padding for alignment with checkbox/radio items.
|
|
37
|
+
*/
|
|
38
|
+
inset = input(false, { transform: booleanAttribute });
|
|
39
|
+
|
|
40
|
+
hostClasses = computed(() =>
|
|
41
|
+
cn(
|
|
42
|
+
'px-2 py-1.5 text-sm font-semibold text-foreground',
|
|
43
|
+
this.inset() && 'pl-8',
|
|
44
|
+
this.class()
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
model,
|
|
5
|
+
} from '@angular/core';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Container for radio menu items. Manages single-selection state.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```html
|
|
12
|
+
* <sng-menu>
|
|
13
|
+
* <sng-menu-radio-group [(value)]="selectedTheme">
|
|
14
|
+
* <sng-menu-radio-item value="light">Light</sng-menu-radio-item>
|
|
15
|
+
* <sng-menu-radio-item value="dark">Dark</sng-menu-radio-item>
|
|
16
|
+
* <sng-menu-radio-item value="system">System</sng-menu-radio-item>
|
|
17
|
+
* </sng-menu-radio-group>
|
|
18
|
+
* </sng-menu>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
@Component({
|
|
22
|
+
selector: 'sng-menu-radio-group',
|
|
23
|
+
standalone: true,
|
|
24
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
25
|
+
host: {
|
|
26
|
+
'role': 'group',
|
|
27
|
+
},
|
|
28
|
+
template: `<ng-content />`,
|
|
29
|
+
})
|
|
30
|
+
export class SngMenuRadioGroup {
|
|
31
|
+
/** The currently selected value. Supports two-way binding. */
|
|
32
|
+
value = model<string>('');
|
|
33
|
+
|
|
34
|
+
/** @internal Called by child radio items to select a value. */
|
|
35
|
+
_selectValue(newValue: string) {
|
|
36
|
+
this.value.set(newValue);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
inject,
|
|
6
|
+
input,
|
|
7
|
+
computed,
|
|
8
|
+
booleanAttribute,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { cn } from './cn';
|
|
11
|
+
import { SngMenuRadioGroup } from './sng-menu-radio-group';
|
|
12
|
+
import { SNG_MENU_PANEL, MENU_ITEM_BASE_CLASSES } from './sng-menu-tokens';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A radio menu item within a radio group.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```html
|
|
19
|
+
* <sng-menu-radio-group [(value)]="theme">
|
|
20
|
+
* <sng-menu-radio-item value="light">Light</sng-menu-radio-item>
|
|
21
|
+
* <sng-menu-radio-item value="dark">Dark</sng-menu-radio-item>
|
|
22
|
+
* </sng-menu-radio-group>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
@Component({
|
|
26
|
+
selector: 'sng-menu-radio-item',
|
|
27
|
+
standalone: true,
|
|
28
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29
|
+
encapsulation: ViewEncapsulation.None,
|
|
30
|
+
host: {
|
|
31
|
+
'[class]': 'hostClasses()',
|
|
32
|
+
'[attr.data-state]': 'isChecked() ? "checked" : "unchecked"',
|
|
33
|
+
'[attr.data-disabled]': 'resolvedDisabled() ? "" : null',
|
|
34
|
+
'[attr.aria-checked]': 'isChecked()',
|
|
35
|
+
'[attr.tabindex]': 'resolvedDisabled() ? -1 : 0',
|
|
36
|
+
'role': 'menuitemradio',
|
|
37
|
+
'(click)': 'select()',
|
|
38
|
+
},
|
|
39
|
+
template: `
|
|
40
|
+
<span class="flex h-4 w-4 items-center justify-center mr-2">
|
|
41
|
+
@if (isChecked()) {
|
|
42
|
+
<svg class="h-2 w-2 fill-current" viewBox="0 0 8 8">
|
|
43
|
+
<circle cx="4" cy="4" r="4" />
|
|
44
|
+
</svg>
|
|
45
|
+
}
|
|
46
|
+
</span>
|
|
47
|
+
<ng-content />
|
|
48
|
+
`,
|
|
49
|
+
})
|
|
50
|
+
export class SngMenuRadioItem {
|
|
51
|
+
private radioGroup = inject(SngMenuRadioGroup, { optional: true });
|
|
52
|
+
private panel = inject(SNG_MENU_PANEL, { optional: true });
|
|
53
|
+
|
|
54
|
+
/** Custom CSS classes. */
|
|
55
|
+
class = input<string>('');
|
|
56
|
+
|
|
57
|
+
/** The value associated with this radio item. */
|
|
58
|
+
value = input.required<string>();
|
|
59
|
+
|
|
60
|
+
/** Whether the radio item is disabled. */
|
|
61
|
+
disabled = input(false, { transform: booleanAttribute });
|
|
62
|
+
|
|
63
|
+
/** Legacy disabled input name. */
|
|
64
|
+
isDisabled = input<unknown>(undefined);
|
|
65
|
+
|
|
66
|
+
/** Whether to close the menu when this item is selected. Uses parent menu's setting when not specified. */
|
|
67
|
+
isCloseOnSelect = input<boolean | undefined>(undefined);
|
|
68
|
+
|
|
69
|
+
resolvedDisabled = computed(() => {
|
|
70
|
+
const legacyValue = this.isDisabled();
|
|
71
|
+
return legacyValue === undefined ? this.disabled() : booleanAttribute(legacyValue);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Whether this item is currently checked. */
|
|
75
|
+
isChecked = computed(() => this.radioGroup?.value() === this.value());
|
|
76
|
+
|
|
77
|
+
hostClasses = computed(() =>
|
|
78
|
+
cn(
|
|
79
|
+
...MENU_ITEM_BASE_CLASSES,
|
|
80
|
+
this.class()
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/** Select this radio item. */
|
|
85
|
+
select() {
|
|
86
|
+
if (!this.resolvedDisabled() && this.radioGroup) {
|
|
87
|
+
this.radioGroup._selectValue(this.value());
|
|
88
|
+
const shouldClose = this.isCloseOnSelect() ?? this.panel?.closeOnSelect() ?? true;
|
|
89
|
+
if (shouldClose) {
|
|
90
|
+
this.panel?.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Visual divider line between groups of menu items.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```html
|
|
8
|
+
* <sng-menu>
|
|
9
|
+
* <sng-menu-item>Undo</sng-menu-item>
|
|
10
|
+
* <sng-menu-item>Redo</sng-menu-item>
|
|
11
|
+
* <sng-menu-separator />
|
|
12
|
+
* <sng-menu-item>Cut</sng-menu-item>
|
|
13
|
+
* <sng-menu-item>Copy</sng-menu-item>
|
|
14
|
+
* </sng-menu>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'sng-menu-separator',
|
|
19
|
+
standalone: true,
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
host: {
|
|
22
|
+
'class': 'block bg-border -mx-1 my-1 h-px',
|
|
23
|
+
'role': 'separator',
|
|
24
|
+
},
|
|
25
|
+
template: '',
|
|
26
|
+
})
|
|
27
|
+
export class SngMenuSeparator {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Displays a shortcut hint aligned to the right side of a menu item.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```html
|
|
8
|
+
* <sng-menu>
|
|
9
|
+
* <sng-menu-item>
|
|
10
|
+
* Save
|
|
11
|
+
* <sng-menu-shortcut>Ctrl+S</sng-menu-shortcut>
|
|
12
|
+
* </sng-menu-item>
|
|
13
|
+
* </sng-menu>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
@Component({
|
|
17
|
+
selector: 'sng-menu-shortcut',
|
|
18
|
+
standalone: true,
|
|
19
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
20
|
+
host: {
|
|
21
|
+
'class': 'ml-auto text-xs tracking-widest text-muted-foreground',
|
|
22
|
+
},
|
|
23
|
+
template: `<ng-content />`,
|
|
24
|
+
})
|
|
25
|
+
export class SngMenuShortcut {}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
input,
|
|
6
|
+
computed,
|
|
7
|
+
inject,
|
|
8
|
+
Injector,
|
|
9
|
+
effect,
|
|
10
|
+
contentChildren,
|
|
11
|
+
forwardRef,
|
|
12
|
+
TemplateRef,
|
|
13
|
+
viewChild,
|
|
14
|
+
ViewContainerRef,
|
|
15
|
+
OnDestroy,
|
|
16
|
+
afterNextRender,
|
|
17
|
+
} from '@angular/core';
|
|
18
|
+
import { Overlay, type OverlayRef, type ConnectedPosition } from '@angular/cdk/overlay';
|
|
19
|
+
import { TemplatePortal } from '@angular/cdk/portal';
|
|
20
|
+
import { cn } from './cn';
|
|
21
|
+
import { SNG_MENU_PANEL, type MenuPanel, animateOverlayClose, focusMenuContent } from './sng-menu-tokens';
|
|
22
|
+
import { SngMenu } from './sng-menu';
|
|
23
|
+
import { SngMenuSub, type MenuContentCoordinator } from './sng-menu-sub';
|
|
24
|
+
|
|
25
|
+
const SUB_POSITIONS: ConnectedPosition[] = [
|
|
26
|
+
{ originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top', offsetX: 4 },
|
|
27
|
+
{ originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top', offsetX: -4 },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Container for submenu items. Uses CDK Overlay for viewport-aware positioning.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```html
|
|
35
|
+
* <sng-menu-sub>
|
|
36
|
+
* <sng-menu-sub-trigger>More</sng-menu-sub-trigger>
|
|
37
|
+
* <sng-menu-sub-content>
|
|
38
|
+
* <sng-menu-item>Nested Item 1</sng-menu-item>
|
|
39
|
+
* </sng-menu-sub-content>
|
|
40
|
+
* </sng-menu-sub>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
@Component({
|
|
44
|
+
selector: 'sng-menu-sub-content',
|
|
45
|
+
standalone: true,
|
|
46
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
47
|
+
encapsulation: ViewEncapsulation.None,
|
|
48
|
+
providers: [
|
|
49
|
+
{ provide: SNG_MENU_PANEL, useExisting: SngMenuSubContent },
|
|
50
|
+
],
|
|
51
|
+
styles: [`
|
|
52
|
+
.sng-menu-sub-panel[data-state=open] { animation: sng-menu-sub-enter 150ms ease both; }
|
|
53
|
+
.sng-menu-sub-panel[data-state=closed] { animation: sng-menu-sub-exit 150ms ease both; }
|
|
54
|
+
@keyframes sng-menu-sub-enter { from { opacity: 0; transform: scale(0.95) translateX(-0.5rem); } }
|
|
55
|
+
@keyframes sng-menu-sub-exit { to { opacity: 0; transform: scale(0.95); } }
|
|
56
|
+
`],
|
|
57
|
+
template: `
|
|
58
|
+
<ng-template #subTemplate>
|
|
59
|
+
<div
|
|
60
|
+
[class]="contentClasses()"
|
|
61
|
+
[attr.data-state]="parentSub.isOpen() ? 'open' : 'closed'"
|
|
62
|
+
role="menu"
|
|
63
|
+
tabindex="-1">
|
|
64
|
+
<ng-content />
|
|
65
|
+
</div>
|
|
66
|
+
</ng-template>
|
|
67
|
+
`,
|
|
68
|
+
})
|
|
69
|
+
export class SngMenuSubContent implements MenuPanel, MenuContentCoordinator, OnDestroy {
|
|
70
|
+
/** @internal */
|
|
71
|
+
parentSub = inject(SngMenuSub);
|
|
72
|
+
private rootPanel = inject(SNG_MENU_PANEL, { skipSelf: true });
|
|
73
|
+
private rootMenu = inject(SngMenu);
|
|
74
|
+
private overlay = inject(Overlay);
|
|
75
|
+
private injector = inject(Injector);
|
|
76
|
+
private viewContainerRef = inject(ViewContainerRef);
|
|
77
|
+
|
|
78
|
+
private subTemplate = viewChild<TemplateRef<unknown>>('subTemplate');
|
|
79
|
+
private overlayRef: OverlayRef | null = null;
|
|
80
|
+
private _closing = false;
|
|
81
|
+
private _hoverEnterHandler: ((e: Event) => void) | null = null;
|
|
82
|
+
private _hoverLeaveHandler: ((e: Event) => void) | null = null;
|
|
83
|
+
|
|
84
|
+
/** @internal Track nested submenus for coordination. */
|
|
85
|
+
private nestedSubs = contentChildren(forwardRef(() => SngMenuSub), { descendants: true });
|
|
86
|
+
|
|
87
|
+
/** @internal Currently open nested submenu. */
|
|
88
|
+
private openSubmenu: SngMenuSub | null = null;
|
|
89
|
+
|
|
90
|
+
/** Custom CSS classes. */
|
|
91
|
+
class = input<string>('');
|
|
92
|
+
|
|
93
|
+
/** Inherits closeOnSelect from the root menu. */
|
|
94
|
+
get closeOnSelect() { return this.rootPanel.closeOnSelect; }
|
|
95
|
+
|
|
96
|
+
contentClasses = computed(() =>
|
|
97
|
+
cn(
|
|
98
|
+
'z-[9999] min-w-[8rem] rounded-md border border-border bg-popover py-1 text-popover-foreground shadow-lg',
|
|
99
|
+
'flex flex-col sng-menu-sub-panel',
|
|
100
|
+
this.class()
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
constructor() {
|
|
105
|
+
// Register with root menu for synchronous cleanup on close
|
|
106
|
+
this.rootMenu._subContentOverlays.add(this);
|
|
107
|
+
|
|
108
|
+
// Register cascade-dispose callback so parent can synchronously tear us down
|
|
109
|
+
this.parentSub._contentDispose = () => this._cascadeDispose();
|
|
110
|
+
|
|
111
|
+
// Register as coordinator for nested submenus
|
|
112
|
+
effect(() => {
|
|
113
|
+
const subs = this.nestedSubs();
|
|
114
|
+
subs.forEach(sub => { sub._parentCoordinator = this; });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// React to parent sub open/close
|
|
118
|
+
effect(() => {
|
|
119
|
+
const isOpen = this.parentSub.isOpen();
|
|
120
|
+
if (isOpen) {
|
|
121
|
+
this.openOverlay();
|
|
122
|
+
} else {
|
|
123
|
+
this.closeOverlay();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Close the entire menu tree. */
|
|
129
|
+
close() {
|
|
130
|
+
this.rootPanel.close();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @internal */
|
|
134
|
+
_requestSubOpen(sub: SngMenuSub): void {
|
|
135
|
+
if (this.openSubmenu && this.openSubmenu !== sub) {
|
|
136
|
+
// Cascade-dispose any nested overlays synchronously before closing the sibling
|
|
137
|
+
this.openSubmenu._contentDispose?.();
|
|
138
|
+
this.openSubmenu.closeImmediate();
|
|
139
|
+
}
|
|
140
|
+
this.openSubmenu = sub;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @internal */
|
|
144
|
+
_notifySubClosed(sub: SngMenuSub): void {
|
|
145
|
+
if (this.openSubmenu === sub) {
|
|
146
|
+
this.openSubmenu = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @internal */
|
|
151
|
+
_shouldOpenImmediately(sub: SngMenuSub): boolean {
|
|
152
|
+
return this.openSubmenu !== null && this.openSubmenu !== sub;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @internal Reset all state when root menu closes. */
|
|
156
|
+
_resetOnMenuClose() {
|
|
157
|
+
this.parentSub._reset();
|
|
158
|
+
if (this.openSubmenu) {
|
|
159
|
+
this.openSubmenu._contentDispose?.();
|
|
160
|
+
this.openSubmenu._reset();
|
|
161
|
+
this.openSubmenu = null;
|
|
162
|
+
}
|
|
163
|
+
this._closing = false;
|
|
164
|
+
this._disposeOverlay();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @internal Cancel pending close on this submenu and all ancestor submenus. */
|
|
168
|
+
_keepAncestorChainOpen() {
|
|
169
|
+
this.parentSub.scheduleOpen();
|
|
170
|
+
const coord = this.parentSub._parentCoordinator;
|
|
171
|
+
if (coord instanceof SngMenuSubContent) {
|
|
172
|
+
coord._keepAncestorChainOpen();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @internal Synchronously tear down this overlay and all nested child overlays. */
|
|
177
|
+
_cascadeDispose() {
|
|
178
|
+
// Cascade down: dispose any open nested submenu's overlay first
|
|
179
|
+
if (this.openSubmenu) {
|
|
180
|
+
this.openSubmenu._contentDispose?.();
|
|
181
|
+
this.openSubmenu._reset();
|
|
182
|
+
this.openSubmenu = null;
|
|
183
|
+
}
|
|
184
|
+
this._closing = false;
|
|
185
|
+
this._disposeOverlay();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
ngOnDestroy() {
|
|
189
|
+
this.parentSub._contentDispose = null;
|
|
190
|
+
this.rootMenu._subContentOverlays.delete(this);
|
|
191
|
+
// Force-dispose: bypass _closing guard since the component is being destroyed
|
|
192
|
+
this._closing = false;
|
|
193
|
+
this._disposeOverlay();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private openOverlay() {
|
|
197
|
+
const template = this.subTemplate();
|
|
198
|
+
const triggerEl = this.parentSub._subTrigger()?._elementRef.nativeElement;
|
|
199
|
+
if (!template || !triggerEl) return;
|
|
200
|
+
|
|
201
|
+
this._disposeOverlay();
|
|
202
|
+
|
|
203
|
+
const positionStrategy = this.overlay.position()
|
|
204
|
+
.flexibleConnectedTo(triggerEl)
|
|
205
|
+
.withPositions(SUB_POSITIONS)
|
|
206
|
+
.withPush(true)
|
|
207
|
+
.withViewportMargin(8);
|
|
208
|
+
|
|
209
|
+
this.overlayRef = this.overlay.create({
|
|
210
|
+
positionStrategy,
|
|
211
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const portal = new TemplatePortal(template, this.viewContainerRef);
|
|
215
|
+
this.overlayRef.attach(portal);
|
|
216
|
+
|
|
217
|
+
// Keep submenu open when mouse is over the overlay panel
|
|
218
|
+
const overlayEl = this.overlayRef.overlayElement;
|
|
219
|
+
this._hoverEnterHandler = () => this._keepAncestorChainOpen();
|
|
220
|
+
this._hoverLeaveHandler = (e: Event) => {
|
|
221
|
+
const related = (e as MouseEvent).relatedTarget as HTMLElement | null;
|
|
222
|
+
// If mouse went back to the parent sng-menu-sub, don't close
|
|
223
|
+
if (related?.closest('sng-menu-sub') === this.parentSub._hostEl) return;
|
|
224
|
+
// If mouse went to a deeper nested submenu panel, don't close
|
|
225
|
+
if (related?.closest('.sng-menu-sub-panel')) return;
|
|
226
|
+
this.parentSub.scheduleClose();
|
|
227
|
+
};
|
|
228
|
+
overlayEl.addEventListener('mouseenter', this._hoverEnterHandler);
|
|
229
|
+
overlayEl.addEventListener('mouseleave', this._hoverLeaveHandler);
|
|
230
|
+
|
|
231
|
+
afterNextRender(() => {
|
|
232
|
+
const panel = this.overlayRef?.overlayElement.querySelector('[role="menu"]') as HTMLElement | null;
|
|
233
|
+
if (panel) {
|
|
234
|
+
focusMenuContent(panel);
|
|
235
|
+
}
|
|
236
|
+
}, { injector: this.injector });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private closeOverlay() {
|
|
240
|
+
if (!this.overlayRef || this._closing) return;
|
|
241
|
+
this._closing = true;
|
|
242
|
+
// Synchronously cascade-close all nested overlays before animating this one
|
|
243
|
+
if (this.openSubmenu) {
|
|
244
|
+
this.openSubmenu._contentDispose?.();
|
|
245
|
+
this.openSubmenu._reset();
|
|
246
|
+
this.openSubmenu = null;
|
|
247
|
+
}
|
|
248
|
+
animateOverlayClose(this.overlayRef, () => this._disposeOverlay());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** @internal */ _disposeOverlay() {
|
|
252
|
+
this._closing = false;
|
|
253
|
+
if (this.overlayRef) {
|
|
254
|
+
const overlayEl = this.overlayRef.overlayElement;
|
|
255
|
+
if (this._hoverEnterHandler) {
|
|
256
|
+
overlayEl.removeEventListener('mouseenter', this._hoverEnterHandler);
|
|
257
|
+
this._hoverEnterHandler = null;
|
|
258
|
+
}
|
|
259
|
+
if (this._hoverLeaveHandler) {
|
|
260
|
+
overlayEl.removeEventListener('mouseleave', this._hoverLeaveHandler);
|
|
261
|
+
this._hoverLeaveHandler = null;
|
|
262
|
+
}
|
|
263
|
+
this.overlayRef.dispose();
|
|
264
|
+
this.overlayRef = null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
ElementRef,
|
|
6
|
+
input,
|
|
7
|
+
inject,
|
|
8
|
+
computed,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { cn } from './cn';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Trigger element for a submenu. Displays a chevron icon indicating
|
|
14
|
+
* a nested menu is available on hover.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```html
|
|
18
|
+
* <sng-menu-sub>
|
|
19
|
+
* <sng-menu-sub-trigger>Share</sng-menu-sub-trigger>
|
|
20
|
+
* <sng-menu-sub-content>
|
|
21
|
+
* <sng-menu-item>Email</sng-menu-item>
|
|
22
|
+
* </sng-menu-sub-content>
|
|
23
|
+
* </sng-menu-sub>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
@Component({
|
|
27
|
+
selector: 'sng-menu-sub-trigger',
|
|
28
|
+
standalone: true,
|
|
29
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
30
|
+
encapsulation: ViewEncapsulation.None,
|
|
31
|
+
host: {
|
|
32
|
+
'[class]': 'hostClasses()',
|
|
33
|
+
'role': 'menuitem',
|
|
34
|
+
'aria-haspopup': 'menu',
|
|
35
|
+
'tabindex': '0',
|
|
36
|
+
},
|
|
37
|
+
template: `
|
|
38
|
+
<ng-content />
|
|
39
|
+
<svg
|
|
40
|
+
class="ml-auto h-4 w-4"
|
|
41
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
42
|
+
viewBox="0 0 24 24"
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
stroke-width="2"
|
|
46
|
+
stroke-linecap="round"
|
|
47
|
+
stroke-linejoin="round"
|
|
48
|
+
>
|
|
49
|
+
<path d="m9 18 6-6-6-6"/>
|
|
50
|
+
</svg>
|
|
51
|
+
`,
|
|
52
|
+
})
|
|
53
|
+
export class SngMenuSubTrigger {
|
|
54
|
+
/** @internal Element ref for overlay positioning. */
|
|
55
|
+
_elementRef = inject(ElementRef);
|
|
56
|
+
|
|
57
|
+
/** Custom CSS classes. */
|
|
58
|
+
class = input<string>('');
|
|
59
|
+
|
|
60
|
+
hostClasses = computed(() =>
|
|
61
|
+
cn(
|
|
62
|
+
'flex cursor-default select-none items-center justify-between gap-4 whitespace-nowrap rounded-sm px-2 py-1.5 text-sm outline-none',
|
|
63
|
+
'hover:bg-accent hover:text-accent-foreground',
|
|
64
|
+
'focus-visible:bg-accent focus-visible:text-accent-foreground',
|
|
65
|
+
this.class()
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
OnDestroy,
|
|
5
|
+
ElementRef,
|
|
6
|
+
inject,
|
|
7
|
+
signal,
|
|
8
|
+
contentChild,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { Subscription, timer } from 'rxjs';
|
|
11
|
+
import { SngMenuSubTrigger } from './sng-menu-sub-trigger';
|
|
12
|
+
|
|
13
|
+
/** Interface for parent content to coordinate sibling submenus. */
|
|
14
|
+
export interface MenuContentCoordinator {
|
|
15
|
+
_requestSubOpen(sub: SngMenuSub): void;
|
|
16
|
+
_notifySubClosed(sub: SngMenuSub): void;
|
|
17
|
+
_shouldOpenImmediately(sub: SngMenuSub): boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Container for a nested submenu within a menu.
|
|
22
|
+
* Opens on hover and contains a trigger and content.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```html
|
|
26
|
+
* <sng-menu-sub>
|
|
27
|
+
* <sng-menu-sub-trigger>More Options</sng-menu-sub-trigger>
|
|
28
|
+
* <sng-menu-sub-content>
|
|
29
|
+
* <sng-menu-item>Option A</sng-menu-item>
|
|
30
|
+
* </sng-menu-sub-content>
|
|
31
|
+
* </sng-menu-sub>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
@Component({
|
|
35
|
+
selector: 'sng-menu-sub',
|
|
36
|
+
standalone: true,
|
|
37
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
38
|
+
host: {
|
|
39
|
+
'class': 'block relative',
|
|
40
|
+
'[attr.data-state]': 'isOpen() ? "open" : "closed"',
|
|
41
|
+
'(mouseenter)': 'scheduleOpen()',
|
|
42
|
+
'(mouseleave)': 'onMouseLeave($event)',
|
|
43
|
+
},
|
|
44
|
+
template: `<ng-content />`,
|
|
45
|
+
})
|
|
46
|
+
export class SngMenuSub implements OnDestroy {
|
|
47
|
+
private hoverTimerSubscription: Subscription | null = null;
|
|
48
|
+
|
|
49
|
+
/** @internal Host element reference. */
|
|
50
|
+
_hostEl = inject(ElementRef).nativeElement;
|
|
51
|
+
|
|
52
|
+
/** @internal The sub-trigger child for positioning. */
|
|
53
|
+
_subTrigger = contentChild(SngMenuSubTrigger);
|
|
54
|
+
|
|
55
|
+
/** @internal Set by parent content for sibling coordination. */
|
|
56
|
+
_parentCoordinator: MenuContentCoordinator | null = null;
|
|
57
|
+
|
|
58
|
+
/** @internal Dispose callback set by content for cascade close. */
|
|
59
|
+
_contentDispose: (() => void) | null = null;
|
|
60
|
+
|
|
61
|
+
isOpen = signal(false);
|
|
62
|
+
|
|
63
|
+
open() {
|
|
64
|
+
if (this.isOpen()) return;
|
|
65
|
+
this.clearTimeout();
|
|
66
|
+
this.isOpen.set(true);
|
|
67
|
+
this._parentCoordinator?._requestSubOpen(this);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
close() {
|
|
71
|
+
if (!this.isOpen()) return;
|
|
72
|
+
this.isOpen.set(false);
|
|
73
|
+
this._parentCoordinator?._notifySubClosed(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Immediately close without animation — used when switching between submenus. */
|
|
77
|
+
closeImmediate() {
|
|
78
|
+
this.clearTimeout();
|
|
79
|
+
this.isOpen.set(false);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
scheduleOpen() {
|
|
83
|
+
this.clearTimeout();
|
|
84
|
+
if (this._parentCoordinator?._shouldOpenImmediately(this)) {
|
|
85
|
+
this.open();
|
|
86
|
+
} else {
|
|
87
|
+
this.hoverTimerSubscription = timer(100).subscribe(() => {
|
|
88
|
+
this.hoverTimerSubscription = null;
|
|
89
|
+
this.open();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @internal */
|
|
95
|
+
onMouseLeave(event: MouseEvent) {
|
|
96
|
+
const related = event.relatedTarget as HTMLElement | null;
|
|
97
|
+
// If mouse moved into the submenu overlay panel, don't close
|
|
98
|
+
if (related?.closest('.sng-menu-sub-panel')) return;
|
|
99
|
+
this.scheduleClose();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
scheduleClose() {
|
|
103
|
+
this.clearTimeout();
|
|
104
|
+
this.hoverTimerSubscription = timer(100).subscribe(() => {
|
|
105
|
+
this.hoverTimerSubscription = null;
|
|
106
|
+
this.close();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @internal Reset all state when root menu closes. */
|
|
111
|
+
_reset() {
|
|
112
|
+
this.clearTimeout();
|
|
113
|
+
this.isOpen.set(false);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private clearTimeout() {
|
|
117
|
+
this.hoverTimerSubscription?.unsubscribe();
|
|
118
|
+
this.hoverTimerSubscription = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ngOnDestroy() {
|
|
122
|
+
this.clearTimeout();
|
|
123
|
+
}
|
|
124
|
+
}
|