Skip to content

Commit

Permalink
PR: Select Items to Rank - Allow users to move items between the rank…
Browse files Browse the repository at this point in the history
…ed and unranked areas using a double click (#8128)
  • Loading branch information
dmitry-kurmanov committed Apr 22, 2024
1 parent e3bda5b commit a4210eb
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
<div [attr.tabindex]="question.getItemTabIndex(model)" [attr.data-sv-drop-target-ranking-item]="index"
[class]="question.getItemClass(model)"
(keydown)="question.handleKeydown($event, model)"
(pointerdown)="question.handlePointerDown($event, model, $any($event.currentTarget))">
(pointerdown)="question.handlePointerDown($event, model, $any($event.currentTarget))"
(pointerup)="question.handlePointerUp($event, model, $any($event.currentTarget))">
<div tabindex="-1" style="outline: none;">
<div [class]="question.cssClasses.itemGhostNode"></div>
<div [class]="question.cssClasses.itemContent">
Expand Down
10 changes: 10 additions & 0 deletions packages/survey-vue3-ui/src/RankingItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@
);
}
"
v-on:pointerup="
(event) => {
question.handlePointerUp.call(
question,
event,
item,
event.currentTarget as HTMLElement
);
}
"
>
<div tabindex="-1" style="outline: none">
<div :class="question.cssClasses.itemGhostNode"></div>
Expand Down
5 changes: 5 additions & 0 deletions src/knockout/koquestion_ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export class QuestionRanking extends QuestionRankingModel {
this.handlePointerDown(event, data, <HTMLElement>event.currentTarget);
return true;
};
public koHandlePointerUp = (data: ItemValue, event: PointerEvent) => {
if(!this.survey.isDesignMode) event.preventDefault();
this.handlePointerUp(event, data, <HTMLElement>event.currentTarget);
return true;
};
}

Serializer.overrideClassCreator("ranking", function() {
Expand Down
2 changes: 1 addition & 1 deletion src/knockout/templates/question-ranking.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

<script type="text/html" id="survey-ranking-item">
<div
data-bind="event: { keydown: question.koHandleKeydown, pointerdown: question.koHandlePointerDown}, css: question.getItemClass($data), attr: {tabindex: question.getItemTabIndex($data), 'data-sv-drop-target-ranking-item': $index() }"
data-bind="event: { keydown: question.koHandleKeydown, pointerdown: question.koHandlePointerDown, pointerup: question.koHandlePointerUp}, css: question.getItemClass($data), attr: {tabindex: question.getItemTabIndex($data), 'data-sv-drop-target-ranking-item': $index() }"
>
<div tabindex="-1" style="outline: none;">
<div data-bind="css: question.cssClasses.itemGhostNode"></div>
Expand Down
55 changes: 43 additions & 12 deletions src/question_ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IsMobile } from "./utils/devices";
import { Helpers } from "./helpers";
import { settings } from "../src/settings";
import { AnimationGroup, IAnimationConsumer } from "./utils/animation";
import { DragOrClickHelper } from "./utils/dragOrClickHelper";

/**
* A class that describes the Ranking question type.
Expand All @@ -18,6 +19,7 @@ import { AnimationGroup, IAnimationConsumer } from "./utils/animation";
*/
export class QuestionRankingModel extends QuestionCheckboxModel {
private domNode: HTMLElement = null;
private dragOrClickHelper: DragOrClickHelper;

constructor(name: string) {
super(name);
Expand All @@ -28,6 +30,7 @@ export class QuestionRankingModel extends QuestionCheckboxModel {
this.setDragDropRankingChoices();
this.updateRankingChoicesSync();
});
this.dragOrClickHelper = new DragOrClickHelper(this.startDrag);
}

protected getDefaultItemComponent(): string {
Expand Down Expand Up @@ -337,6 +340,8 @@ export class QuestionRankingModel extends QuestionCheckboxModel {
return new DragDropRankingChoices(this.survey, null, this.longTap);
}

private draggedChoise: ItemValue;
private draggedTargetNode: HTMLElement;
public handlePointerDown = (
event: PointerEvent,
choice: ItemValue,
Expand All @@ -353,7 +358,26 @@ export class QuestionRankingModel extends QuestionCheckboxModel {
this.canStartDragDueItemEnabled(choice)
)
{
this.dragDropRankingChoices.startDrag(event, choice, this, node);
this.draggedChoise = choice;
this.draggedTargetNode = node;
this.dragOrClickHelper.onPointerDown(event);
}
};

public startDrag = (event: PointerEvent): void => {
this.dragDropRankingChoices.startDrag(event, this.draggedChoise, this, this.draggedTargetNode);
}

public handlePointerUp = (
event: PointerEvent,
choice: ItemValue,
node: HTMLElement
): void => {
if (!this.selectToRankEnabled) return;
if (
this.allowStartDrag
) {
this.handleKeydownSelectToRank(<any>event, choice, " ", false);
}
};

Expand Down Expand Up @@ -438,9 +462,11 @@ export class QuestionRankingModel extends QuestionCheckboxModel {
}, 1);
}

public handleKeydownSelectToRank(event: KeyboardEvent, movedElement: ItemValue): void {
public handleKeydownSelectToRank(event: KeyboardEvent, movedElement: ItemValue, hardKey?:string, isNeedFocus: boolean = true): void {
if (this.isDesignMode) return;
const key: any = event.key;

let key: any = event.key;
if (hardKey) key = hardKey;
if(key !== " " && key !== "ArrowUp" && key !== "ArrowDown") return;

const dnd:any = this.dragDropRankingChoices; //????
Expand All @@ -452,11 +478,12 @@ export class QuestionRankingModel extends QuestionCheckboxModel {
let toIndex;

if (key === " " && !isMovedElementRanked) {
toIndex = 0;
if (!this.checkMaxSelectedChoicesUnreached() || !this.canStartDragDueItemEnabled(movedElement)) return;
toIndex = this.value.length;
this.animationAllowed = false;
dnd.selectToRank(this, fromIndex, toIndex);
this.animationAllowed = true;
this.setValueAfterKeydown(toIndex, "to-container");
this.setValueAfterKeydown(toIndex, "to-container", isNeedFocus);
return;
}
if(!isMovedElementRanked) return;
Expand All @@ -465,23 +492,27 @@ export class QuestionRankingModel extends QuestionCheckboxModel {
dnd.unselectFromRank(this, fromIndex);
this.animationAllowed = true;
toIndex = this.unRankingChoices.indexOf(movedElement); //'this.' leads to actual array after the 'unselectFromRank' method
this.setValueAfterKeydown(toIndex, "from-container");
this.setValueAfterKeydown(toIndex, "from-container", isNeedFocus);
return;
}
const delta = key === "ArrowUp" ? -1 : (key === "ArrowDown" ? 1 : 0);
if(delta === 0) return;
toIndex = fromIndex + delta;
if(toIndex < 0 || toIndex >= rankingChoices.length) return;
dnd.reorderRankedItem(this, fromIndex, toIndex);
this.setValueAfterKeydown(toIndex, "to-container");
this.setValueAfterKeydown(toIndex, "to-container", isNeedFocus);
}

private setValueAfterKeydown(index: number, container: string) {
private setValueAfterKeydown(index: number, container: string, isNeedFocus: boolean = true) {
this.setValue();
setTimeout(() => {
this.focusItem(index, container);
}, 1);
event.preventDefault();

if (isNeedFocus) {
setTimeout(() => {
this.focusItem(index, container);
}, 1);
}

event && event.preventDefault();
}

private focusItem = (index: number, container?: string) => {
Expand Down
16 changes: 16 additions & 0 deletions src/react/reactquestion_ranking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ export class SurveyQuestionRanking extends SurveyQuestionElementBase {
event.currentTarget
);
},
(event: any) => {
event.persist();
//event.preventDefault();
this.question.handlePointerUp.call(
this.question,
event,
item,
event.currentTarget
);
},
this.question.cssClasses,
this.question.getItemClass(item),
this.question,
Expand All @@ -82,6 +92,7 @@ export class SurveyQuestionRanking extends SurveyQuestionElementBase {
i: number,
handleKeydown: (event: any) => void,
handlePointerDown: (event: PointerEvent) => void,
handlePointerUp: (event: PointerEvent) => void,
cssClasses: any,
itemClass: string,
question: QuestionRankingModel,
Expand All @@ -101,6 +112,7 @@ export class SurveyQuestionRanking extends SurveyQuestionElementBase {
itemTabIndex={tabIndex}
handleKeydown={handleKeydown}
handlePointerDown={handlePointerDown}
handlePointerUp={handlePointerUp}
cssClasses={cssClasses}
itemClass={itemClass}
question={question}
Expand Down Expand Up @@ -133,6 +145,9 @@ export class SurveyQuestionRankingItem extends ReactSurveyElement {
protected get handlePointerDown(): (event: any) => void {
return this.props.handlePointerDown;
}
protected get handlePointerUp(): (event: any) => void {
return this.props.handlePointerUp;
}
protected get cssClasses(): any {
return this.props.cssClasses;
}
Expand Down Expand Up @@ -167,6 +182,7 @@ export class SurveyQuestionRankingItem extends ReactSurveyElement {
className={this.itemClass}
onKeyDown={this.handleKeydown}
onPointerDown={this.handlePointerDown}
onPointerUp={this.handlePointerUp}
data-sv-drop-target-ranking-item={this.index}
>
<div tabIndex={-1} style={{ outline: "none" }}>
Expand Down
2 changes: 1 addition & 1 deletion src/vue/ranking/ranking-item.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div :tabindex="question.getItemTabIndex(item)" :data-sv-drop-target-ranking-item="index" :class="question.getItemClass(item)" v-on:keydown="(event)=>{question.handleKeydown.call(question, event, item)}" v-on:pointerdown="(event)=>{question.handlePointerDown.call(question, event, item, event.currentTarget)}">
<div :tabindex="question.getItemTabIndex(item)" :data-sv-drop-target-ranking-item="index" :class="question.getItemClass(item)" v-on:keydown="(event)=>{question.handleKeydown.call(question, event, item)}" v-on:pointerdown="(event)=>{question.handlePointerDown.call(question, event, item, event.currentTarget)}" v-on:pointerup="(event)=>{question.handlePointerUp.call(question, event, item, event.currentTarget)}">
<div tabindex="-1" style="outline: none;">
<div :class="cssClasses.itemGhostNode" />
<div :class="cssClasses.itemContent">
Expand Down
24 changes: 23 additions & 1 deletion testCafe/questions/ranking.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ frameworks.forEach((framework) => {
.find("span")
.withText("two");

await t.dragToElement(FirstItem, SecondItem);
await t.dragToElement(FirstItem, SecondItem, { speed: 0.1 });

let data = await getData();
await t.expect(data[newName]).eql([
Expand Down Expand Up @@ -311,4 +311,26 @@ frameworks.forEach((framework) => {

await removeFlexboxLayout();
});

test("ranking: selectToRank: click to add", async (t) => {
const setSelectToRankEnabled = ClientFunction(() => {
const rankingQ = window["survey"].getAllQuestions()[0];
rankingQ.selectToRankEnabled = true;
});
await setSelectToRankEnabled();
await t.click(PriceItem);
await t.click(BatteryItem);

let data = await getData();
await t.expect(data["smartphone-features"]).eql([
"Price",
"Battery life"
]);

const setSelectToRankDisabled = ClientFunction(() => {
const rankingQ = window["survey"].getAllQuestions()[0];
rankingQ.selectToRankEnabled = false;
});
await setSelectToRankDisabled();
});
});
18 changes: 17 additions & 1 deletion tests/question_ranking_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ QUnit.test("Ranking: selectToRank key navigation with animation", function (asse

q.handleKeydown(<any>{ key: " ", preventDefault: () => {} }, q.choices[0]);
assert.deepEqual(q.unRankingChoices.map((item) => item.value), ["c"]);
assert.deepEqual(q.rankingChoices.map((item) => item.value), ["a", "b"]);
assert.deepEqual(q.rankingChoices.map((item) => item.value), ["b", "a"]);

q.handleKeydown(<any>{ key: " ", preventDefault: () => {} }, q.choices[1]);
assert.deepEqual(q.unRankingChoices.map((item) => item.value), ["b", "c"]);
Expand Down Expand Up @@ -511,6 +511,22 @@ QUnit.test("selectToRankEnabled : checkMaxSelectedChoicesUnreached", function (a
assert.equal(questionModel.checkMaxSelectedChoicesUnreached(), false, "MaxSelectedChoices limit reached");
});

QUnit.test("selectToRankEnabled : checkMaxSelectedChoices and handleKeydownSelectToRank", function (assert) {
const selectToRankEnabled = true;
const withDefaultValue = true;
const questionModel = createRankingQuestionModel(selectToRankEnabled, withDefaultValue);

questionModel.maxSelectedChoices = 2;
const fakeEvent:any = { key: " ", preventDefault: ()=>{} };
questionModel.handleKeydownSelectToRank(fakeEvent, questionModel.unRankingChoices[0], " ", false);

assert.equal(questionModel.value.length, 2, "can't add due to MaxSelectedChoices");

questionModel.handleKeydownSelectToRank(fakeEvent, questionModel.rankingChoices[0], " ", false);

assert.equal(questionModel.value.length, 1, "unrank with MaxSelectedChoices");
});

QUnit.test("Ranking: renderedSelectToRankAreasLayout", function (assert) {
const selectToRankEnabled = true;
const withDefaultValue = false;
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion visualRegressionTests/tests/defaultV2/ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ frameworks.forEach(framework => {
});

await patchDragDropToShowGhostElementAfterDrop();
await t.dragToElement(item1, item1);
await t.dragToElement(item1, item1, { destinationOffsetX: -1, speed: 0.1 });
await takeElementScreenshot("question-ranking-shortcut-position-container-scroll-layout.png", Selector(".sd-question"), t, comparer);
});
});
Expand Down

0 comments on commit a4210eb

Please sign in to comment.