Skip to content

Commit 8c336c9

Browse files
authored
fix(cdk/drag-drop): preserve checked state for grouped radio inputs (#20237)
We transfer input `value` property while cloning draggable element, but with several radio inputs that have the same `name` attribute we can face an issue when checked state moves to cloned radio input element and we can't restore it when dropping element. These changes add unique name for all cloned elements: radio inputs inside preview and placeholder. This makes sure that original input won't lose its checked state once the clone is inserted in the DOM. Fixes #20236
1 parent 490a893 commit 8c336c9

File tree

2 files changed

+67
-1
lines changed

2 files changed

+67
-1
lines changed

src/cdk/drag-drop/clone-node.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,19 @@ function transferData<T extends Element>(selector: string, node: HTMLElement, cl
4444
}
4545
}
4646

47+
// Counter for unique cloned radio button names.
48+
let cloneUniqueId = 0;
49+
4750
/** Transfers the data of one input element to another. */
48-
function transferInputData(source: Element & {value: string}, clone: Element & {value: string}) {
51+
function transferInputData(source: Element & {value: string},
52+
clone: Element & {value: string; name: string; type: string}) {
4953
clone.value = source.value;
54+
// Radio button `name` attributes must be unique for radio button groups
55+
// otherwise original radio buttons can lose their checked state
56+
// once the clone is inserted in the DOM.
57+
if (clone.type === 'radio' && clone.name) {
58+
clone.name = `mat-clone-${clone.name}-${cloneUniqueId++}`;
59+
}
5060
}
5161

5262
/** Transfers the data of one canvas element to another. */

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,6 +2272,37 @@ describe('CdkDrag', () => {
22722272
expect(previewSelect.value).toBe(value);
22732273
}));
22742274

2275+
it('should preserve checked state for radio inputs in the content', fakeAsync(() => {
2276+
const fixture = createComponent(DraggableWithRadioInputsInDropZone);
2277+
fixture.detectChanges();
2278+
const item = fixture.componentInstance.dragItems.toArray()[2].element.nativeElement;
2279+
const sourceRadioInput =
2280+
item.querySelector<HTMLInputElement>('input[type="radio"]')!;
2281+
2282+
expect(sourceRadioInput.checked).toBeTruthy();
2283+
2284+
startDraggingViaMouse(fixture, item);
2285+
2286+
const preview = document.querySelector('.cdk-drag-preview')!;
2287+
const previewRadioInput = preview.querySelector<HTMLInputElement>('input[type="radio"]')!;
2288+
expect(previewRadioInput.checked).toBeTruthy(
2289+
'Expected cloned radio input in preview has the same state as original radio input');
2290+
2291+
const placeholder = document.querySelector('.cdk-drag-placeholder')!;
2292+
const placeholderRadioInput =
2293+
placeholder.querySelector<HTMLInputElement>('input[type="radio"]')!;
2294+
expect(placeholderRadioInput.checked).toBeTruthy(
2295+
'Expected cloned radio input in placeholder has the same state as original radio input');
2296+
2297+
dispatchMouseEvent(document, 'mouseup');
2298+
// Important to tick with 0 since we don't want to flush any pending timeouts.
2299+
// It also makes sure that all clones have been removed from the DOM.
2300+
tick(0);
2301+
2302+
expect(sourceRadioInput.checked)
2303+
.toBeTruthy('Expected original radio input has preserved its original checked state');
2304+
}));
2305+
22752306
it('should clear the ids from descendants of the preview', fakeAsync(() => {
22762307
const fixture = createComponent(DraggableInDropZone);
22772308
fixture.detectChanges();
@@ -6463,6 +6494,31 @@ class DraggableWithInputsInDropZone extends DraggableInDropZone {
64636494
}
64646495

64656496

6497+
@Component({
6498+
template: `
6499+
<div
6500+
cdkDropList
6501+
class="drop-list scroll-container"
6502+
[cdkDropListData]="items">
6503+
<div
6504+
*ngFor="let item of items"
6505+
cdkDrag
6506+
[cdkDragData]="item">
6507+
{{item.id}}
6508+
<input type="radio" name="radio" [checked]="item.checked"/>
6509+
</div>
6510+
</div>
6511+
`
6512+
})
6513+
class DraggableWithRadioInputsInDropZone {
6514+
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
6515+
items = [
6516+
{id: 1, checked: false},
6517+
{id: 2, checked: false},
6518+
{id: 3, checked: true},
6519+
];
6520+
}
6521+
64666522
/**
64676523
* Drags an element to a position on the page using the mouse.
64686524
* @param fixture Fixture on which to run change detection.

0 commit comments

Comments
 (0)