From ec877f4eca388d823dd21d527187f92275350e56 Mon Sep 17 00:00:00 2001 From: Janik Schumacher Date: Fri, 13 Dec 2024 17:37:06 +0100 Subject: [PATCH 1/3] first draft --- apps/showcase/angular.json | 2 +- .../pages/landing/themesection.component.ts | 133 +++--------------- .../primeng/src/autocomplete/autocomplete.ts | 19 +-- .../src/basecomponent/basecomponent.ts | 3 +- .../src/cascadeselect/cascadeselect.ts | 19 +-- .../primeng/src/confirmpopup/confirmpopup.ts | 23 +-- .../primeng/src/contextmenu/contextmenu.ts | 23 +-- packages/primeng/src/dialog/dialog.ts | 44 +++--- packages/primeng/src/dropdown/dropdown.ts | 24 ++-- packages/primeng/src/megamenu/megamenu.ts | 19 +-- packages/primeng/src/menubar/menubar.ts | 22 +-- .../primeng/src/multiselect/multiselect.ts | 25 ++-- packages/primeng/src/popover/popover.ts | 23 +-- packages/primeng/src/select/select.ts | 26 ++-- packages/primeng/src/speeddial/speeddial.ts | 32 +++-- packages/primeng/src/table/table.ts | 1 + packages/primeng/src/tieredmenu/tieredmenu.ts | 28 ++-- .../primeng/src/utils/closeonescapeservice.ts | 48 +++++++ packages/primeng/src/utils/public_api.ts | 5 +- 19 files changed, 244 insertions(+), 275 deletions(-) create mode 100644 packages/primeng/src/utils/closeonescapeservice.ts diff --git a/apps/showcase/angular.json b/apps/showcase/angular.json index 4a80bd487ca..cace2df3459 100644 --- a/apps/showcase/angular.json +++ b/apps/showcase/angular.json @@ -63,7 +63,7 @@ "development": { "optimization": false, "extractLicenses": false, - "sourceMap": false + "sourceMap": true } }, "defaultConfiguration": "production" diff --git a/apps/showcase/pages/landing/themesection.component.ts b/apps/showcase/pages/landing/themesection.component.ts index dc8efdc6cb6..98d1a9a5f44 100644 --- a/apps/showcase/pages/landing/themesection.component.ts +++ b/apps/showcase/pages/landing/themesection.component.ts @@ -3,11 +3,13 @@ import { Customer } from '@/domain/customer'; import { AppConfigService } from '@/service/appconfigservice'; import { CustomerService } from '@/service/customerservice'; import { CommonModule } from '@angular/common'; -import { Component, Inject, PLATFORM_ID, ViewChild } from '@angular/core'; +import { Component, Inject, PLATFORM_ID, signal, ViewChild } from '@angular/core'; import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; import { IconField } from 'primeng/iconfield'; import { InputIcon } from 'primeng/inputicon'; import { InputTextModule } from 'primeng/inputtext'; +import { MultiSelectModule } from 'primeng/multiselect'; import { ProgressBar } from 'primeng/progressbar'; import { Table, TableModule } from 'primeng/table'; import { Tag } from 'primeng/tag'; @@ -15,129 +17,24 @@ import { Tag } from 'primeng/tag'; @Component({ selector: 'theme-section', standalone: true, - imports: [CommonModule, TableModule, ButtonModule, Tag, ProgressBar, InputTextModule, IconField, InputIcon], + imports: [CommonModule, TableModule, ButtonModule, Tag, ProgressBar, InputTextModule, IconField, InputIcon, MultiSelectModule, DialogModule], template: `
Components

The most complete UI component library for Angular based on a design-agnostic infrastructure.

- - -
- Customers - - - - - - -
-
- - - - - - -
- Name - -
- - -
- Country - -
- - -
- Agent - -
- - -
- Date - -
- - -
- Balance - -
- - -
- Status - -
- - -
- Activity - -
- - - -
- - - - - - - {{ customer.name }} - - -
- country flag - {{ customer.country.name }} -
- - -
- representative - {{ customer.representative.name }} -
- - - {{ customer.date | date: 'MM/dd/yyyy' }} - - - {{ customer.balance | currency: 'USD' : 'symbol' }} - - - - - - - - - - - -
- - - No customers found. - - -
+ + + + +


+
- ` + `, + styles: '::ng-deep .p-dialog-content { height: 100%; }' }) export class ThemeSectionComponent { constructor( @@ -151,6 +48,12 @@ export class ThemeSectionComponent { customers: Customer[]; + protected readonly visible = signal(true); + + public hide() { + this.visible.set(false); + } + selectedCustomers: Customer[]; loading: boolean = true; diff --git a/packages/primeng/src/autocomplete/autocomplete.ts b/packages/primeng/src/autocomplete/autocomplete.ts index 537302115d2..64f7488f831 100755 --- a/packages/primeng/src/autocomplete/autocomplete.ts +++ b/packages/primeng/src/autocomplete/autocomplete.ts @@ -40,6 +40,7 @@ import { Overlay } from 'primeng/overlay'; import { Ripple } from 'primeng/ripple'; import { Scroller } from 'primeng/scroller'; import { Nullable } from 'primeng/ts-helpers'; +import { CloseOnEscapeService } from 'primeng/utils'; import { AutoCompleteCompleteEvent, AutoCompleteDropdownClickEvent, AutoCompleteLazyLoadEvent, AutoCompleteSelectEvent, AutoCompleteUnselectEvent } from './autocomplete.interface'; import { AutoCompleteStyle } from './style/autocompletestyle'; @@ -970,6 +971,13 @@ export class AutoComplete extends BaseComponent implements AfterViewChecked, Aft private zone: NgZone ) { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.hide(), + kind: 'single' + }, + this.injector + ); effect(() => { this.filled = isNotEmpty(this.modelValue()); }); @@ -1354,10 +1362,6 @@ export class AutoComplete extends BaseComponent implements AfterViewChecked, Aft this.onEnterKey(event); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event); break; @@ -1486,11 +1490,6 @@ export class AutoComplete extends BaseComponent implements AfterViewChecked, Aft event.preventDefault(); } - onEscapeKey(event) { - this.overlayVisible && this.hide(true); - event.preventDefault(); - } - onTabKey(event) { if (this.focusedOptionIndex() !== -1) { this.onOptionSelect(event, this.visibleOptions()[this.focusedOptionIndex()]); @@ -1654,6 +1653,7 @@ export class AutoComplete extends BaseComponent implements AfterViewChecked, Aft } hide(isFocus = false) { + const isVisible = this.overlayVisible; const _hide = () => { this.dirty = isFocus; this.overlayVisible = false; @@ -1666,6 +1666,7 @@ export class AutoComplete extends BaseComponent implements AfterViewChecked, Aft setTimeout(() => { _hide(); }, 0); // For ScreenReaders + return isVisible; } clear() { diff --git a/packages/primeng/src/basecomponent/basecomponent.ts b/packages/primeng/src/basecomponent/basecomponent.ts index 145b0a9ee2b..fded9d7e7f7 100644 --- a/packages/primeng/src/basecomponent/basecomponent.ts +++ b/packages/primeng/src/basecomponent/basecomponent.ts @@ -1,8 +1,7 @@ import { DOCUMENT, isPlatformServer } from '@angular/common'; -import { ChangeDetectorRef, ContentChildren, Directive, ElementRef, inject, Injector, Input, PLATFORM_ID, QueryList, Renderer2, SimpleChanges } from '@angular/core'; +import { ChangeDetectorRef, Directive, ElementRef, inject, Injector, Input, PLATFORM_ID, Renderer2, SimpleChanges } from '@angular/core'; import { Theme, ThemeService } from '@primeuix/styled'; import { getKeyValue, uuid } from '@primeuix/utils'; -import { PrimeTemplate } from 'primeng/api'; import { Base, BaseStyle } from 'primeng/base'; import { PrimeNG } from 'primeng/config'; import { BaseComponentStyle } from './style/basecomponentstyle'; diff --git a/packages/primeng/src/cascadeselect/cascadeselect.ts b/packages/primeng/src/cascadeselect/cascadeselect.ts index bd431e99d6e..602595ab450 100755 --- a/packages/primeng/src/cascadeselect/cascadeselect.ts +++ b/packages/primeng/src/cascadeselect/cascadeselect.ts @@ -33,6 +33,7 @@ import { AngleRightIcon, ChevronDownIcon, TimesIcon } from 'primeng/icons'; import { Overlay } from 'primeng/overlay'; import { Ripple } from 'primeng/ripple'; import { Nullable, VoidListener } from 'primeng/ts-helpers'; +import { CloseOnEscapeService } from 'primeng/utils'; import { CascadeSelectBeforeHideEvent, CascadeSelectBeforeShowEvent, CascadeSelectChangeEvent, CascadeSelectHideEvent, CascadeSelectShowEvent } from './cascadeselect.interface'; import { CascadeSelectStyle } from './style/cascadeselectstyle'; @@ -966,10 +967,6 @@ export class CascadeSelect extends BaseComponent implements OnInit, AfterContent this.onEnterKey(event); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event); break; @@ -1108,11 +1105,6 @@ export class CascadeSelect extends BaseComponent implements OnInit, AfterContent this.onEnterKey(event); } - onEscapeKey(event) { - this.overlayVisible && this.hide(event, true); - event.preventDefault(); - } - onTabKey(event) { if (this.focusedOptionInfo().index !== -1) { const processedOption = this.visibleOptions()[this.focusedOptionInfo().index]; @@ -1352,6 +1344,7 @@ export class CascadeSelect extends BaseComponent implements OnInit, AfterContent } hide(event?, isFocus = false) { + if (!this.overlayVisible) return false; const _hide = () => { this.overlayVisible = false; this.clicked = false; @@ -1366,6 +1359,7 @@ export class CascadeSelect extends BaseComponent implements OnInit, AfterContent setTimeout(() => { _hide(); }, 0); // For ScreenReaders + return true; } show(event?, isFocus = false) { @@ -1435,6 +1429,13 @@ export class CascadeSelect extends BaseComponent implements OnInit, AfterContent constructor(public overlayService: OverlayService) { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.hide(), + kind: 'single' + }, + this.injector + ); effect(() => { const activeOptionPath = this.activeOptionPath(); if (isNotEmpty(activeOptionPath)) { diff --git a/packages/primeng/src/confirmpopup/confirmpopup.ts b/packages/primeng/src/confirmpopup/confirmpopup.ts index 408a8db4f99..871b0fa033a 100755 --- a/packages/primeng/src/confirmpopup/confirmpopup.ts +++ b/packages/primeng/src/confirmpopup/confirmpopup.ts @@ -10,7 +10,6 @@ import { ContentChildren, ElementRef, EventEmitter, - HostListener, Inject, inject, Input, @@ -28,7 +27,7 @@ import { BaseComponent } from 'primeng/basecomponent'; import { ButtonModule } from 'primeng/button'; import { ConnectedOverlayScrollHandler } from 'primeng/dom'; import { Nullable, VoidListener } from 'primeng/ts-helpers'; -import { ZIndexUtils } from 'primeng/utils'; +import { CloseOnEscapeService, ZIndexUtils } from 'primeng/utils'; import { Subscription } from 'rxjs'; import { ConfirmPopupStyle } from './style/confirmpopupstyle'; @@ -222,6 +221,19 @@ export class ConfirmPopup extends BaseComponent implements AfterContentInit, OnD @Inject(DOCUMENT) public document: Document ) { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => { + if (this.confirmation && this.confirmation.closeOnEscape) { + this.onReject(); + return true; + } + return false; + }, + kind: 'single' + }, + this.injector + ); this.window = this.document.defaultView as Window; this.subscription = this.confirmationService.requireConfirmation$.subscribe((confirmation) => { if (!confirmation) { @@ -288,13 +300,6 @@ export class ConfirmPopup extends BaseComponent implements AfterContentInit, OnD return undefined; } - @HostListener('document:keydown.escape', ['$event']) - onEscapeKeydown(event: KeyboardEvent) { - if (this.confirmation && this.confirmation.closeOnEscape) { - this.onReject(); - } - } - onAnimationStart(event: AnimationEvent) { if (event.toState === 'open') { this.container = event.element; diff --git a/packages/primeng/src/contextmenu/contextmenu.ts b/packages/primeng/src/contextmenu/contextmenu.ts index 3c7687c1220..a1a4041c69f 100755 --- a/packages/primeng/src/contextmenu/contextmenu.ts +++ b/packages/primeng/src/contextmenu/contextmenu.ts @@ -22,6 +22,7 @@ import { QueryList, signal, TemplateRef, + untracked, ViewChild, ViewEncapsulation, ViewRef @@ -54,7 +55,7 @@ import { AngleRightIcon } from 'primeng/icons'; import { Ripple } from 'primeng/ripple'; import { TooltipModule } from 'primeng/tooltip'; import { VoidListener } from 'primeng/ts-helpers'; -import { ZIndexUtils } from 'primeng/utils'; +import { CloseOnEscapeService, ZIndexUtils } from 'primeng/utils'; import { ContextMenuStyle } from './style/contextmenustyle'; @Component({ @@ -572,6 +573,13 @@ export class ContextMenu extends BaseComponent implements OnInit, AfterContentIn constructor(public overlayService: OverlayService) { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.closeWithEscape(), + kind: 'single' + }, + this.injector + ); effect(() => { const path = this.activeItemPath(); @@ -825,10 +833,6 @@ export class ContextMenu extends BaseComponent implements OnInit, AfterContentIn this.onEnterKey(event); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event); break; @@ -921,13 +925,12 @@ export class ContextMenu extends BaseComponent implements OnInit, AfterContentIn this.onEnterKey(event); } - onEscapeKey(event: KeyboardEvent) { - this.hide(); + private closeWithEscape() { + const didHide = this.hide(); const processedItem = this.findVisibleItem(this.findFirstFocusedItemIndex()); const focusedItemInfo = this.focusedItemInfo(); this.focusedItemInfo.set({ ...focusedItemInfo, index: this.findFirstFocusedItemIndex(), item: processedItem.item }); - - event.preventDefault(); + return didHide; } onTabKey(event: KeyboardEvent) { @@ -1054,10 +1057,12 @@ export class ContextMenu extends BaseComponent implements OnInit, AfterContentIn } hide() { + const isVisibile = untracked(this.visible); this.visible.set(false); this.onHide.emit(); this.activeItemPath.set([]); this.focusedItemInfo.set({ index: -1, level: 0, parentKey: '', item: null }); + return isVisibile; } toggle(event?: any) { diff --git a/packages/primeng/src/dialog/dialog.ts b/packages/primeng/src/dialog/dialog.ts index 60868781de4..ac2285a7162 100755 --- a/packages/primeng/src/dialog/dialog.ts +++ b/packages/primeng/src/dialog/dialog.ts @@ -31,7 +31,7 @@ import { DomHandler } from 'primeng/dom'; import { FocusTrap } from 'primeng/focustrap'; import { TimesIcon, WindowMaximizeIcon, WindowMinimizeIcon } from 'primeng/icons'; import { Nullable, VoidListener } from 'primeng/ts-helpers'; -import { ZIndexUtils } from 'primeng/utils'; +import { CloseOnEscapeService, ZIndexUtils } from 'primeng/utils'; import { DialogStyle } from './style/dialogstyle'; const showAnimation = animation([style({ transform: '{{transform}}', opacity: 0 }), animate('{{transition}}')]); @@ -534,8 +534,6 @@ export class Dialog extends BaseComponent implements OnInit, AfterContentInit, O documentResizeEndListener: VoidListener; - documentEscapeListener: VoidListener; - maskClickListener: VoidListener; lastPageX: number | undefined; @@ -602,6 +600,17 @@ export class Dialog extends BaseComponent implements OnInit, AfterContentInit, O }; } + constructor() { + super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.closeWithEscape(), + kind: 'global', + onlyCloseForFocussedElements: () => [this.wrapper] + }, + this.injector + ); + } ngOnInit() { super.ngOnInit(); if (this.breakpoints) { @@ -698,6 +707,13 @@ export class Dialog extends BaseComponent implements OnInit, AfterContentInit, O event.preventDefault(); } + private closeWithEscape() { + if (!this.closeOnEscape || !this.visible || !this.closable) { + return false; + } + this.visibleChange.emit(false); + } + enableModality() { if (this.closable && this.dismissableMask) { this.maskClickListener = this.renderer.listen(this.wrapper, 'mousedown', (event: any) => { @@ -924,17 +940,12 @@ export class Dialog extends BaseComponent implements OnInit, AfterContentInit, O if (this.resizable) { this.bindDocumentResizeListeners(); } - - if (this.closeOnEscape && this.closable) { - this.bindDocumentEscapeListener(); - } } unbindGlobalListeners() { this.unbindDocumentDragListener(); this.unbindDocumentDragEndListener(); this.unbindDocumentResizeListeners(); - this.unbindDocumentEscapeListener(); } bindDocumentDragListener() { @@ -985,23 +996,6 @@ export class Dialog extends BaseComponent implements OnInit, AfterContentInit, O } } - bindDocumentEscapeListener() { - const documentTarget: any = this.el ? this.el.nativeElement.ownerDocument : 'document'; - - this.documentEscapeListener = this.renderer.listen(documentTarget, 'keydown', (event) => { - if (event.key == 'Escape') { - this.close(event); - } - }); - } - - unbindDocumentEscapeListener() { - if (this.documentEscapeListener) { - this.documentEscapeListener(); - this.documentEscapeListener = null; - } - } - appendContainer() { if (this.appendTo) { if (this.appendTo === 'body') this.renderer.appendChild(this.document.body, this.wrapper); diff --git a/packages/primeng/src/dropdown/dropdown.ts b/packages/primeng/src/dropdown/dropdown.ts index 9495e664348..2c497d7c85b 100755 --- a/packages/primeng/src/dropdown/dropdown.ts +++ b/packages/primeng/src/dropdown/dropdown.ts @@ -59,6 +59,7 @@ import { Ripple } from 'primeng/ripple'; import { Scroller } from 'primeng/scroller'; import { TooltipModule } from 'primeng/tooltip'; import { Nullable } from 'primeng/ts-helpers'; +import { CloseOnEscapeService } from 'primeng/utils'; import { DropdownChangeEvent, DropdownFilterEvent, DropdownFilterOptions, DropdownLazyLoadEvent } from './dropdown.interface'; import { DropdownStyle } from './style/dropdownstyle'; @@ -1033,6 +1034,13 @@ export class Dropdown extends BaseComponent implements OnInit, AfterViewInit, Af public filterService: FilterService ) { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.hide(), + kind: 'single' + }, + this.injector + ); effect(() => { const modelValue = this.modelValue(); const visibleOptions = this.visibleOptions(); @@ -1428,6 +1436,7 @@ export class Dropdown extends BaseComponent implements OnInit, AfterViewInit, Af * @group Method */ public hide(isFocus?) { + const isVisibile = this.overlayVisible; this.overlayVisible = false; this.focusedOptionIndex.set(-1); this.clicked.set(false); @@ -1448,6 +1457,7 @@ export class Dropdown extends BaseComponent implements OnInit, AfterViewInit, Af } } this.cd.markForCheck(); + return isVisibile; } onInputFocus(event: Event) { @@ -1526,11 +1536,6 @@ export class Dropdown extends BaseComponent implements OnInit, AfterViewInit, Af this.onEnterKey(event); break; - //escape and tab - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event); break; @@ -1584,10 +1589,6 @@ export class Dropdown extends BaseComponent implements OnInit, AfterViewInit, Af this.onEnterKey(event, true); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event, true); break; @@ -1807,11 +1808,6 @@ export class Dropdown extends BaseComponent implements OnInit, AfterViewInit, Af event.preventDefault(); } - onEscapeKey(event: KeyboardEvent) { - this.overlayVisible && this.hide(true); - event.preventDefault(); - } - onTabKey(event, pressedInInputText = false) { if (!pressedInInputText) { if (this.overlayVisible && this.hasFocusableElements()) { diff --git a/packages/primeng/src/megamenu/megamenu.ts b/packages/primeng/src/megamenu/megamenu.ts index 7b8eaaf946c..74fe3b54f62 100755 --- a/packages/primeng/src/megamenu/megamenu.ts +++ b/packages/primeng/src/megamenu/megamenu.ts @@ -32,7 +32,7 @@ import { AngleDownIcon, AngleRightIcon, BarsIcon } from 'primeng/icons'; import { Ripple } from 'primeng/ripple'; import { TooltipModule } from 'primeng/tooltip'; import { VoidListener } from 'primeng/ts-helpers'; -import { ZIndexUtils } from 'primeng/utils'; +import { CloseOnEscapeService, ZIndexUtils } from 'primeng/utils'; import { MegaMenuStyle } from './style/megamenustyle'; @Component({ @@ -629,6 +629,13 @@ export class MegaMenu extends BaseComponent implements AfterContentInit, OnDestr constructor() { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.closeWithEscape(), + kind: 'single' + }, + this.injector + ); effect(() => { const activeItem = this.activeItem(); if (isNotEmpty(activeItem)) { @@ -902,10 +909,6 @@ export class MegaMenu extends BaseComponent implements AfterContentInit, OnDestr this.onEnterKey(event); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event); break; @@ -1165,13 +1168,13 @@ export class MegaMenu extends BaseComponent implements AfterContentInit, OnDestr this.onEnterKey(event); } - onEscapeKey(event: KeyboardEvent) { + private closeWithEscape() { if (isNotEmpty(this.activeItem())) { this.focusedItemInfo.set({ index: this.activeItem().index, key: this.activeItem().key, item: this.activeItem().item }); this.activeItem.set(null); + return true; } - - event.preventDefault(); + return false; } onTabKey(event: KeyboardEvent) { diff --git a/packages/primeng/src/menubar/menubar.ts b/packages/primeng/src/menubar/menubar.ts index b2894e1d0bc..ed97728ff27 100755 --- a/packages/primeng/src/menubar/menubar.ts +++ b/packages/primeng/src/menubar/menubar.ts @@ -36,7 +36,7 @@ import { AngleDownIcon, AngleRightIcon, BarsIcon } from 'primeng/icons'; import { Ripple } from 'primeng/ripple'; import { TooltipModule } from 'primeng/tooltip'; import { VoidListener } from 'primeng/ts-helpers'; -import { ZIndexUtils } from 'primeng/utils'; +import { CloseOnEscapeService, ZIndexUtils } from 'primeng/utils'; import { interval, Subject, Subscription } from 'rxjs'; import { debounce, filter } from 'rxjs/operators'; import { MenuBarStyle } from './style/menubarstyle'; @@ -562,6 +562,13 @@ export class Menubar extends BaseComponent implements AfterContentInit, OnDestro private menubarService: MenubarService ) { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.closeWithEscape(), + kind: 'single' + }, + this.injector + ); effect(() => { const path = this.activeItemPath(); @@ -803,6 +810,7 @@ export class Menubar extends BaseComponent implements AfterContentInit, OnDestro } hide(event?, isFocus?: boolean) { + const isVisible = this.activeItemPath().length > 0; if (this.mobileActive) { setTimeout(() => { focus(this.menubutton.nativeElement); @@ -814,6 +822,7 @@ export class Menubar extends BaseComponent implements AfterContentInit, OnDestro isFocus && focus(this.rootmenu?.menubarViewChild.nativeElement); this.dirty = false; + return isVisible; } show() { @@ -875,10 +884,6 @@ export class Menubar extends BaseComponent implements AfterContentInit, OnDestro this.onEnterKey(event); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event); break; @@ -1097,11 +1102,10 @@ export class Menubar extends BaseComponent implements AfterContentInit, OnDestro this.onEnterKey(event); } - onEscapeKey(event: KeyboardEvent) { - this.hide(event, true); + private closeWithEscape() { + const didHide = this.hide(undefined, true); this.focusedItemInfo().index = this.findFirstFocusedItemIndex(); - - event.preventDefault(); + return didHide; } onTabKey(event: KeyboardEvent) { diff --git a/packages/primeng/src/multiselect/multiselect.ts b/packages/primeng/src/multiselect/multiselect.ts index 8595e534c59..0d08e8aa65b 100755 --- a/packages/primeng/src/multiselect/multiselect.ts +++ b/packages/primeng/src/multiselect/multiselect.ts @@ -64,6 +64,7 @@ import { Ripple } from 'primeng/ripple'; import { Scroller } from 'primeng/scroller'; import { Tooltip } from 'primeng/tooltip'; import { Nullable } from 'primeng/ts-helpers'; +import { CloseOnEscapeService } from 'primeng/utils'; import { MultiSelectBlurEvent, MultiSelectChangeEvent, MultiSelectFilterEvent, MultiSelectFilterOptions, MultiSelectFocusEvent, MultiSelectLazyLoadEvent, MultiSelectRemoveEvent, MultiSelectSelectAllChangeEvent } from './multiselect.interface'; import { MultiSelectStyle } from './style/multiselectstyle'; @@ -1325,6 +1326,13 @@ export class MultiSelect extends BaseComponent implements OnInit, AfterViewInit, public overlayService: OverlayService ) { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.hide(), + kind: 'single' + }, + this.injector + ); effect(() => { const modelValue = this.modelValue(); @@ -1639,10 +1647,6 @@ export class MultiSelect extends BaseComponent implements OnInit, AfterViewInit, this.onEnterKey(event); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event); break; @@ -1702,10 +1706,6 @@ export class MultiSelect extends BaseComponent implements OnInit, AfterViewInit, this.onEnterKey(event); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event, true); break; @@ -1828,11 +1828,6 @@ export class MultiSelect extends BaseComponent implements OnInit, AfterViewInit, event.preventDefault(); } - onEscapeKey(event) { - this.overlayVisible && this.hide(true); - event.preventDefault(); - } - onDeleteKey(event: KeyboardEvent) { if (this.showClear) { this.clear(event); @@ -2089,6 +2084,9 @@ export class MultiSelect extends BaseComponent implements OnInit, AfterViewInit, * @group Method */ public hide(isFocus?) { + if (!this.overlayVisible) { + return false; + } this.overlayVisible = false; this.focusedOptionIndex.set(-1); @@ -2102,6 +2100,7 @@ export class MultiSelect extends BaseComponent implements OnInit, AfterViewInit, isFocus && focus(this.focusInputViewChild?.nativeElement); this.onPanelHide.emit(); this.cd.markForCheck(); + return true; } onOverlayAnimationStart(event: AnimationEvent) { diff --git a/packages/primeng/src/popover/popover.ts b/packages/primeng/src/popover/popover.ts index 54f91a3d7fb..45609e3f095 100755 --- a/packages/primeng/src/popover/popover.ts +++ b/packages/primeng/src/popover/popover.ts @@ -9,7 +9,6 @@ import { ContentChildren, ElementRef, EventEmitter, - HostListener, inject, Input, NgModule, @@ -26,10 +25,8 @@ import { absolutePosition, addClass, appendChild, findSingle, getOffset, isIOS, import { OverlayService, PrimeTemplate, SharedModule } from 'primeng/api'; import { BaseComponent } from 'primeng/basecomponent'; import { ConnectedOverlayScrollHandler } from 'primeng/dom'; -import { TimesIcon } from 'primeng/icons'; -import { Ripple } from 'primeng/ripple'; import { Nullable, VoidListener } from 'primeng/ts-helpers'; -import { ZIndexUtils } from 'primeng/utils'; +import { CloseOnEscapeService, ZIndexUtils } from 'primeng/utils'; import { Subscription } from 'rxjs'; import { PopoverStyle } from './style/popoverstyle'; @@ -214,6 +211,17 @@ export class Popover extends BaseComponent implements AfterContentInit, OnDestro overlayService = inject(OverlayService); + constructor() { + super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.hide(), + kind: 'single' + }, + this.injector + ); + } + ngAfterContentInit() { this.templates.forEach((item) => { switch (item.getType()) { @@ -422,8 +430,10 @@ export class Popover extends BaseComponent implements AfterContentInit, OnDestro * @group Method */ hide() { + const isVisibile = this.overlayVisible; this.overlayVisible = false; this.cd.markForCheck(); + return isVisibile; } onCloseClick(event: MouseEvent) { @@ -431,11 +441,6 @@ export class Popover extends BaseComponent implements AfterContentInit, OnDestro event.preventDefault(); } - @HostListener('document:keydown.escape', ['$event']) - onEscapeKeydown(event: KeyboardEvent) { - this.hide(); - } - onWindowResize() { if (this.overlayVisible && !isTouchDevice()) { this.hide(); diff --git a/packages/primeng/src/select/select.ts b/packages/primeng/src/select/select.ts index b84f0ed5a9f..b097a933180 100755 --- a/packages/primeng/src/select/select.ts +++ b/packages/primeng/src/select/select.ts @@ -60,6 +60,7 @@ import { Ripple } from 'primeng/ripple'; import { Scroller } from 'primeng/scroller'; import { Tooltip } from 'primeng/tooltip'; import { Nullable } from 'primeng/ts-helpers'; +import { CloseOnEscapeService } from 'primeng/utils'; import { SelectChangeEvent, SelectFilterEvent, SelectFilterOptions, SelectLazyLoadEvent } from './select.interface'; import { SelectStyle } from './style/selectstyle'; @@ -1164,6 +1165,13 @@ export class Select extends BaseComponent implements OnInit, AfterViewInit, Afte public filterService: FilterService ) { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.hide(), + kind: 'single' + }, + this.injector + ); effect(() => { const modelValue = this.modelValue(); const visibleOptions = this.visibleOptions(); @@ -1568,6 +1576,9 @@ export class Select extends BaseComponent implements OnInit, AfterViewInit, Afte * @group Method */ public hide(isFocus?) { + if (!this.overlayVisible) { + return false; + } this.overlayVisible = false; this.focusedOptionIndex.set(-1); this.clicked.set(false); @@ -1588,6 +1599,7 @@ export class Select extends BaseComponent implements OnInit, AfterViewInit, Afte } } this.cd.markForCheck(); + return true; } onInputFocus(event: Event) { @@ -1666,11 +1678,6 @@ export class Select extends BaseComponent implements OnInit, AfterViewInit, Afte this.onEnterKey(event); break; - //escape and tab - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event); break; @@ -1724,10 +1731,6 @@ export class Select extends BaseComponent implements OnInit, AfterViewInit, Afte this.onEnterKey(event, true); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event, true); break; @@ -1947,11 +1950,6 @@ export class Select extends BaseComponent implements OnInit, AfterViewInit, Afte event.preventDefault(); } - onEscapeKey(event: KeyboardEvent) { - this.overlayVisible && this.hide(true); - event.preventDefault(); - } - onTabKey(event, pressedInInputText = false) { if (!pressedInInputText) { if (this.overlayVisible && this.hasFocusableElements()) { diff --git a/packages/primeng/src/speeddial/speeddial.ts b/packages/primeng/src/speeddial/speeddial.ts index 6a458b4a0cf..92ff26d4d56 100644 --- a/packages/primeng/src/speeddial/speeddial.ts +++ b/packages/primeng/src/speeddial/speeddial.ts @@ -29,6 +29,7 @@ import { ButtonModule, ButtonProps } from 'primeng/button'; import { PlusIcon } from 'primeng/icons'; import { Ripple } from 'primeng/ripple'; import { TooltipModule } from 'primeng/tooltip'; +import { CloseOnEscapeService } from 'primeng/utils'; import { asapScheduler } from 'rxjs'; import { SpeedDialStyle } from './style/speeddialstyle'; @@ -336,6 +337,17 @@ export class SpeedDial extends BaseComponent implements AfterViewInit, AfterCont return _style ? _style({ props: this }) : {}; } + constructor() { + super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.closeWithEscape(), + kind: 'single' + }, + this.injector + ); + } + getTooltipOptions(item: MenuItem) { return { ...this.tooltipOptions, tooltipLabel: item.label, disabled: !this.tooltipOptions }; } @@ -388,12 +400,14 @@ export class SpeedDial extends BaseComponent implements AfterViewInit, AfterCont } hide() { + const isVisible = this._visible; this.onVisibleChange.emit(false); this.visibleChange.emit(false); this._visible = false; this.onHide.emit(); this.unbindDocumentClickListener(); this.cd.markForCheck(); + return isVisible; } onButtonClick(event: MouseEvent) { @@ -435,10 +449,6 @@ export class SpeedDial extends BaseComponent implements AfterViewInit, AfterCont this.onEnterKey(event); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Home': this.onHomeKey(event); break; @@ -533,12 +543,11 @@ export class SpeedDial extends BaseComponent implements AfterViewInit, AfterCont buttonEl && focus(buttonEl); } - onEscapeKey(event: KeyboardEvent) { - this.hide(); - + private closeWithEscape() { + const didHide = this.hide(); const buttonEl = findSingle(this.container.nativeElement, 'button'); - buttonEl && focus(buttonEl); + return didHide; } onTogglerKeydown(event: KeyboardEvent) { @@ -546,18 +555,11 @@ export class SpeedDial extends BaseComponent implements AfterViewInit, AfterCont case 'ArrowDown': case 'ArrowLeft': this.onTogglerArrowDown(event); - break; case 'ArrowUp': case 'ArrowRight': this.onTogglerArrowUp(event); - - break; - - case 'Escape': - this.onEscapeKey(event); - break; default: diff --git a/packages/primeng/src/table/table.ts b/packages/primeng/src/table/table.ts index bed2fed6dbd..9679c83c3dc 100644 --- a/packages/primeng/src/table/table.ts +++ b/packages/primeng/src/table/table.ts @@ -4518,6 +4518,7 @@ export class EditableColumn implements OnChanges, AfterViewInit, OnDestroy { } event.preventDefault(); + event.stopPropagation(); } } diff --git a/packages/primeng/src/tieredmenu/tieredmenu.ts b/packages/primeng/src/tieredmenu/tieredmenu.ts index 7862bf498a5..74a00520bd8 100755 --- a/packages/primeng/src/tieredmenu/tieredmenu.ts +++ b/packages/primeng/src/tieredmenu/tieredmenu.ts @@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, Component, ContentChild, + ContentChildren, ElementRef, EventEmitter, Inject, @@ -12,6 +13,7 @@ import { OnDestroy, OnInit, Output, + QueryList, Renderer2, TemplateRef, ViewChild, @@ -23,9 +25,7 @@ import { inject, input, numberAttribute, - signal, - ContentChildren, - QueryList + signal } from '@angular/core'; import { RouterModule } from '@angular/router'; import { absolutePosition, appendChild, findLastIndex, findSingle, focus, isEmpty, isNotEmpty, isPrintableCharacter, isTouchDevice, nestedPosition, relativePosition, resolve, uuid } from '@primeuix/utils'; @@ -36,7 +36,7 @@ import { AngleRightIcon } from 'primeng/icons'; import { Ripple } from 'primeng/ripple'; import { TooltipModule } from 'primeng/tooltip'; import { Nullable, VoidListener } from 'primeng/ts-helpers'; -import { ZIndexUtils } from 'primeng/utils'; +import { CloseOnEscapeService, ZIndexUtils } from 'primeng/utils'; import { TieredMenuStyle } from './style/tieredmenustyle'; @Component({ @@ -576,6 +576,13 @@ export class TieredMenu extends BaseComponent implements OnInit, OnDestroy { constructor(public overlayService: OverlayService) { super(); + inject(CloseOnEscapeService).closeOnEscape( + { + closeOnEscape: () => this.closeWithEscape(), + kind: 'single' + }, + this.injector + ); effect(() => { const path = this.activeItemPath(); @@ -789,10 +796,6 @@ export class TieredMenu extends BaseComponent implements OnInit, OnDestroy { this.onEnterKey(event); break; - case 'Escape': - this.onEscapeKey(event); - break; - case 'Tab': this.onTabKey(event); break; @@ -886,11 +889,10 @@ export class TieredMenu extends BaseComponent implements OnInit, OnDestroy { this.onEnterKey(event); } - onEscapeKey(event: KeyboardEvent) { - this.hide(event, true); + private closeWithEscape() { + const didClose = this.hide(undefined, true); this.focusedItemInfo().index = this.findFirstFocusedItemIndex(); - - event.preventDefault(); + return didClose; } onTabKey(event: KeyboardEvent) { @@ -1019,6 +1021,7 @@ export class TieredMenu extends BaseComponent implements OnInit, OnDestroy { * @group Method */ hide(event?, isFocus?: boolean) { + const isVisible = this.visible; if (this.popup) { this.onHide.emit({}); this.visible = false; @@ -1028,6 +1031,7 @@ export class TieredMenu extends BaseComponent implements OnInit, OnDestroy { isFocus && focus(this.relatedTarget || this.target || this.rootmenu.sublistViewChild.nativeElement); this.dirty = false; + return isVisible; } /** diff --git a/packages/primeng/src/utils/closeonescapeservice.ts b/packages/primeng/src/utils/closeonescapeservice.ts new file mode 100644 index 00000000000..6aef8e6f7a8 --- /dev/null +++ b/packages/primeng/src/utils/closeonescapeservice.ts @@ -0,0 +1,48 @@ +import { DestroyRef, Injectable, Injector } from '@angular/core'; + +type ClosableWithEscape = ClosableWithEscapeSingle | ClosableWithEscapeGlobal; + +type ClosableWithEscapeSingle = { + kind: 'single'; + closeOnEscape(event: Event): boolean; +}; +type ClosableWithEscapeGlobal = { + kind: 'global'; + closeOnEscape(event: Event): void; + onlyCloseForFocussedElements?: () => HTMLElement[]; +}; + +@Injectable({ providedIn: 'root' }) +export class CloseOnEscapeService { + private readonly _targets = new Set(); + + constructor() { + document.addEventListener('keydown', (event) => { + if (event.key !== 'Escape') { + return; + } + const singleTargets: ClosableWithEscapeSingle[] = Array.from(this._targets).filter((target) => target.kind === 'single') as ClosableWithEscapeSingle[]; + if (singleTargets.length > 0) { + const closed = singleTargets.map((target) => target.closeOnEscape(event)); + if (closed.some((closed) => closed)) { + return; + } + } + const globalTargets: ClosableWithEscapeGlobal[] = Array.from(this._targets).filter((target) => target.kind === 'global') as ClosableWithEscapeGlobal[]; + const activeElement = document.activeElement ?? document.body; + globalTargets + .filter((x) => !x.onlyCloseForFocussedElements || activeElement === document.body || x.onlyCloseForFocussedElements().some((x) => x.contains(activeElement))) + .forEach((target) => { + target.closeOnEscape(event); + }); + }); + } + + public closeOnEscape(target: ClosableWithEscape, injector: Injector) { + this._targets.add(target); + const destroyRef = injector.get(DestroyRef); + destroyRef.onDestroy(() => { + this._targets.delete(target); + }); + } +} diff --git a/packages/primeng/src/utils/public_api.ts b/packages/primeng/src/utils/public_api.ts index 792687032b5..af81bb1ee06 100644 --- a/packages/primeng/src/utils/public_api.ts +++ b/packages/primeng/src/utils/public_api.ts @@ -1,6 +1,7 @@ +import { CloseOnEscapeService } from './closeonescapeservice'; +import { transformToBoolean, transformToNumber } from './inpututils'; import { ObjectUtils } from './objectutils'; import { UniqueComponentId } from './uniquecomponentid'; import ZIndexUtils from './zindexutils'; -import { transformToBoolean, transformToNumber } from './inpututils'; -export { ZIndexUtils, UniqueComponentId, ObjectUtils, transformToNumber, transformToBoolean }; +export { CloseOnEscapeService, ObjectUtils, transformToBoolean, transformToNumber, UniqueComponentId, ZIndexUtils }; From 59940a58a6ff8ea1273235a08545d9d20d04872f Mon Sep 17 00:00:00 2001 From: Janik Schumacher Date: Tue, 17 Dec 2024 15:54:00 +0100 Subject: [PATCH 2/3] fixes --- .../pages/landing/themesection.component.ts | 19 ++++++++++++++----- .../primeng/src/utils/closeonescapeservice.ts | 12 ++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/showcase/pages/landing/themesection.component.ts b/apps/showcase/pages/landing/themesection.component.ts index 98d1a9a5f44..db89b050c31 100644 --- a/apps/showcase/pages/landing/themesection.component.ts +++ b/apps/showcase/pages/landing/themesection.component.ts @@ -25,9 +25,13 @@ import { Tag } from 'primeng/tag';
- + + - + +


+
+


@@ -48,10 +52,15 @@ export class ThemeSectionComponent { customers: Customer[]; - protected readonly visible = signal(true); + protected readonly visible1 = signal(true); + + public hide1() { + this.visible1.set(false); + } + protected readonly visible2 = signal(true); - public hide() { - this.visible.set(false); + public hide2() { + this.visible2.set(false); } selectedCustomers: Customer[]; diff --git a/packages/primeng/src/utils/closeonescapeservice.ts b/packages/primeng/src/utils/closeonescapeservice.ts index 6aef8e6f7a8..a7d9553f404 100644 --- a/packages/primeng/src/utils/closeonescapeservice.ts +++ b/packages/primeng/src/utils/closeonescapeservice.ts @@ -1,4 +1,5 @@ -import { DestroyRef, Injectable, Injector } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { DestroyRef, inject, Injectable, Injector } from '@angular/core'; type ClosableWithEscape = ClosableWithEscapeSingle | ClosableWithEscapeGlobal; @@ -17,6 +18,7 @@ export class CloseOnEscapeService { private readonly _targets = new Set(); constructor() { + const document = inject(DOCUMENT); document.addEventListener('keydown', (event) => { if (event.key !== 'Escape') { return; @@ -31,7 +33,13 @@ export class CloseOnEscapeService { const globalTargets: ClosableWithEscapeGlobal[] = Array.from(this._targets).filter((target) => target.kind === 'global') as ClosableWithEscapeGlobal[]; const activeElement = document.activeElement ?? document.body; globalTargets - .filter((x) => !x.onlyCloseForFocussedElements || activeElement === document.body || x.onlyCloseForFocussedElements().some((x) => x.contains(activeElement))) + .filter((x) => { + if (!x.onlyCloseForFocussedElements || activeElement === document.body) { + return true; + } + const onlyCloseFor = x.onlyCloseForFocussedElements(); + return onlyCloseFor.some((x) => x?.contains(activeElement)); + }) .forEach((target) => { target.closeOnEscape(event); }); From 031b2800fde15c2e4f4664aa829089172f405bb2 Mon Sep 17 00:00:00 2001 From: Janik Schumacher Date: Tue, 17 Dec 2024 16:02:38 +0100 Subject: [PATCH 3/3] . --- .../pages/landing/themesection.component.ts | 142 ++++++++++++++---- 1 file changed, 115 insertions(+), 27 deletions(-) diff --git a/apps/showcase/pages/landing/themesection.component.ts b/apps/showcase/pages/landing/themesection.component.ts index db89b050c31..dc8efdc6cb6 100644 --- a/apps/showcase/pages/landing/themesection.component.ts +++ b/apps/showcase/pages/landing/themesection.component.ts @@ -3,13 +3,11 @@ import { Customer } from '@/domain/customer'; import { AppConfigService } from '@/service/appconfigservice'; import { CustomerService } from '@/service/customerservice'; import { CommonModule } from '@angular/common'; -import { Component, Inject, PLATFORM_ID, signal, ViewChild } from '@angular/core'; +import { Component, Inject, PLATFORM_ID, ViewChild } from '@angular/core'; import { ButtonModule } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; import { IconField } from 'primeng/iconfield'; import { InputIcon } from 'primeng/inputicon'; import { InputTextModule } from 'primeng/inputtext'; -import { MultiSelectModule } from 'primeng/multiselect'; import { ProgressBar } from 'primeng/progressbar'; import { Table, TableModule } from 'primeng/table'; import { Tag } from 'primeng/tag'; @@ -17,28 +15,129 @@ import { Tag } from 'primeng/tag'; @Component({ selector: 'theme-section', standalone: true, - imports: [CommonModule, TableModule, ButtonModule, Tag, ProgressBar, InputTextModule, IconField, InputIcon, MultiSelectModule, DialogModule], + imports: [CommonModule, TableModule, ButtonModule, Tag, ProgressBar, InputTextModule, IconField, InputIcon], template: `
Components

The most complete UI component library for Angular based on a design-agnostic infrastructure.

- - - - - -


-
- -


-
+ + +
+ Customers + + + + + + +
+
+ + + + + + +
+ Name + +
+ + +
+ Country + +
+ + +
+ Agent + +
+ + +
+ Date + +
+ + +
+ Balance + +
+ + +
+ Status + +
+ + +
+ Activity + +
+ + + +
+ + + + + + + {{ customer.name }} + + +
+ country flag + {{ customer.country.name }} +
+ + +
+ representative + {{ customer.representative.name }} +
+ + + {{ customer.date | date: 'MM/dd/yyyy' }} + + + {{ customer.balance | currency: 'USD' : 'symbol' }} + + + + + + + + + + + +
+ + + No customers found. + + +
- `, - styles: '::ng-deep .p-dialog-content { height: 100%; }' + ` }) export class ThemeSectionComponent { constructor( @@ -52,17 +151,6 @@ export class ThemeSectionComponent { customers: Customer[]; - protected readonly visible1 = signal(true); - - public hide1() { - this.visible1.set(false); - } - protected readonly visible2 = signal(true); - - public hide2() { - this.visible2.set(false); - } - selectedCustomers: Customer[]; loading: boolean = true;