Skip to content

Commit 956c6d3

Browse files
fix(dialog): make dialog titles selectable
closes #19428
1 parent f11b0ce commit 956c6d3

4 files changed

Lines changed: 50 additions & 1 deletion

File tree

packages/primeng/src/dialog/dialog.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ describe('Dialog', () => {
308308
expect(dialogInstance.keepInViewport).toBe(true);
309309
expect(dialogInstance.rtl).toBe(false);
310310
expect(dialogInstance.role).toBe('dialog');
311+
expect(dialogInstance.selectableTitle).toBe(false);
311312
});
312313

313314
it('should accept custom input values', async () => {
@@ -1275,6 +1276,34 @@ describe('Dialog', () => {
12751276
}
12761277
});
12771278

1279+
it('should not initiate drag when mousedown is on title with selectableTitle enabled', async () => {
1280+
component.visible = true;
1281+
fixture.changeDetectorRef.markForCheck();
1282+
await fixture.whenStable();
1283+
await new Promise((resolve) => setTimeout(resolve, 0));
1284+
1285+
dialogInstance.selectableTitle = true;
1286+
spyOn(dialogInstance, 'initDrag');
1287+
1288+
const titleEl = fixture.debugElement.query(By.css('.p-dialog-title'));
1289+
titleEl?.nativeElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
1290+
expect(dialogInstance.initDrag).not.toHaveBeenCalled();
1291+
});
1292+
1293+
it('should still initiate drag from non-title header area with selectableTitle enabled', async () => {
1294+
component.visible = true;
1295+
fixture.changeDetectorRef.markForCheck();
1296+
await fixture.whenStable();
1297+
await new Promise((resolve) => setTimeout(resolve, 0));
1298+
1299+
dialogInstance.selectableTitle = true;
1300+
spyOn(dialogInstance, 'initDrag');
1301+
1302+
const header = fixture.debugElement.query(By.css('.p-dialog-header'));
1303+
header?.nativeElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
1304+
expect(dialogInstance.initDrag).toHaveBeenCalled();
1305+
});
1306+
12781307
it('should handle resize initialization', async () => {
12791308
component.resizable = true;
12801309
fixture.changeDetectorRef.markForCheck();

packages/primeng/src/dialog/dialog.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const DIALOG_INSTANCE = new InjectionToken<Dialog>('DIALOG_INSTANCE');
9696
<ng-template #notHeadless>
9797
<div *ngIf="resizable" [class]="cx('resizeHandle')" [pBind]="ptm('resizeHandle')" [style.z-index]="90" (mousedown)="initResize($event)"></div>
9898
<div #titlebar [class]="cx('header')" [pBind]="ptm('header')" (mousedown)="initDrag($event)" *ngIf="showHeader">
99-
<span [id]="ariaLabelledBy" [class]="cx('title')" [pBind]="ptm('title')" *ngIf="!_headerTemplate && !headerTemplate && !headerT">{{ header }}</span>
99+
<span [id]="ariaLabelledBy" [class]="cx('title')" [pBind]="ptm('title')" *ngIf="!_headerTemplate && !headerTemplate && !headerT" (mousedown)="selectableTitle ? $event.stopPropagation() : null">{{ header }}</span>
100100
<ng-container *ngTemplateOutlet="_headerTemplate || headerTemplate || headerT; context: { ariaLabelledBy: ariaLabelledBy }"></ng-container>
101101
<div [class]="cx('headerActions')" [pBind]="ptm('headerActions')">
102102
<p-button
@@ -301,6 +301,11 @@ export class Dialog extends BaseComponent<DialogPassThrough> implements OnInit,
301301
* @group Props
302302
*/
303303
@Input({ transform: booleanAttribute }) focusTrap: boolean = true;
304+
/**
305+
* When enabled, the title text is selectable and dragging only works on other parts of the header.
306+
* @group Props
307+
*/
308+
@Input({ transform: booleanAttribute }) selectableTitle: boolean = false;
304309
/**
305310
* Transition options of the animation.
306311
* @deprecated since v21.0.0. Use `motionOptions` instead.
@@ -842,6 +847,15 @@ export class Dialog extends BaseComponent<DialogPassThrough> implements OnInit,
842847
}
843848

844849
if (this.draggable) {
850+
if (this.selectableTitle) {
851+
// Clear any selected title text when the user starts dragging from a non-title area.
852+
// anchorNode is where the selection started and contains() ensures we only clear
853+
// selections inside this dialog, leaving any selections outside untouched.
854+
const selection = this.document.defaultView?.getSelection();
855+
if (selection?.rangeCount && this.container()?.contains(selection.anchorNode)) {
856+
selection.removeAllRanges();
857+
}
858+
}
845859
this.dragging = true;
846860
this.lastPageX = event.pageX;
847861
this.lastPageY = event.pageY;

packages/primeng/src/dynamicdialog/dynamicdialog-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ export class DynamicDialogConfig<DataType = any, InputValuesType extends Record<
126126
* @group Props
127127
*/
128128
draggable?: boolean = false;
129+
/**
130+
* When enabled, the title text is selectable and dragging only works on other parts of the header.
131+
* @group Props
132+
*/
133+
selectableTitle?: boolean = false;
129134
/**
130135
* Keeps dialog in the viewport.
131136
* @group Props

packages/primeng/src/dynamicdialog/dynamicdialog.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const DYNAMIC_DIALOG_INSTANCE = new InjectionToken<DynamicDialog>('DYNAMIC_DIALO
4141
[maximizable]="maximizable"
4242
[keepInViewport]="keepInViewport"
4343
[focusTrap]="ddconfig?.focusTrap !== false"
44+
[selectableTitle]="ddconfig?.selectableTitle"
4445
[transitionOptions]="ddconfig?.transitionOptions || '150ms cubic-bezier(0, 0, 0.2, 1)'"
4546
[closeAriaLabel]="ddconfig?.closeAriaLabel || defaultCloseAriaLabel"
4647
[minimizeIcon]="minimizeIcon"

0 commit comments

Comments
 (0)