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.
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
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
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
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
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
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
@@ -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
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
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
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.