Skip to content

Commit

Permalink
feat: implement slider interaction with correct response indication a…
Browse files Browse the repository at this point in the history
…nd add corresponding story
  • Loading branch information
Marcelh1983 committed Feb 13, 2025
1 parent 4fd1ea6 commit 57a51f7
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 9 deletions.
22 changes: 22 additions & 0 deletions public/assets/qti-item/example-slider.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<qti-assessment-item
xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqtiasi_v3p0 https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0p1_v1p0.xsd"
identifier="slider-1" title="Slider Interaction – Water"
adaptive="false" time-dependent="false" xml:lang="en-US">
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="integer">
<qti-correct-response>
<qti-value>70</qti-value>
</qti-correct-response>
</qti-response-declaration>
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float" />
<qti-item-body>
<qti-slider-interaction response-identifier="RESPONSE"
lower-bound="0" upper-bound="100" step="10" orientation="horizontal">
<qti-prompt> Roughly (to the nearest 10%) what percentage of the Earth's surface is
covered in water? </qti-prompt>
</qti-slider-interaction>
</qti-item-body>
<qti-response-processing
template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response.xml" />
</qti-assessment-item>
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ export default css`
border-radius: 9999px; /* rounded-full */
left: var(--value-percentage);
}
[part='knob-correct'] {
background-color: #c8e6c9;
border: 2px solid #66bb6a;
position: relative;
height: 1rem; /* h-4 */
width: 1rem; /* w-4 */
transform-origin: center;
transform: translateX(-50%);
cursor: pointer;
border-radius: 9999px; /* rounded-full */
left: var(--value-percentage-correct);
}
[part='value'] {
position: absolute;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import { html, LitElement } from 'lit';
import { html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';

import styles from './qti-slider-interaction.styles';
import { Interaction } from '../../../exports/interaction';

import type { CSSResultGroup} from 'lit';
import type { ResponseVariable } from '../../../exports/variables';
import type { CSSResultGroup } from 'lit';

@customElement('qti-slider-interaction')
export class QtiSliderInteraction extends LitElement {
export class QtiSliderInteraction extends Interaction {
static formAssociated = true; // Enables elementInternals for forms

static styles: CSSResultGroup = styles;

private _value = 0;
private _internals: ElementInternals;
private _correctResponseNumber: number | null = null;

@query('#rail') private _rail!: HTMLElement;

@property({ type: Number, attribute: 'lower-bound' }) min = 0;
@property({ type: Number, attribute: 'upper-bound' }) max = 100;
@property({ type: Number, attribute: 'step' }) step = 1;

constructor() {
super();
this._internals = this.attachInternals();
validate(): boolean {
return true;
}

override connectedCallback() {
Expand All @@ -43,6 +44,23 @@ export class QtiSliderInteraction extends LitElement {
}
}

public toggleCorrectResponse(responseVariable: ResponseVariable, show: boolean) {
if (show) {
this._correctResponse = responseVariable.correctResponse.toString();
const nr = parseFloat(responseVariable.correctResponse.toString());
if (!isNaN(nr)) {
this._correctResponseNumber = nr;
const valuePercentage = ((this._correctResponseNumber - this.min) / (this.max - this.min)) * 100;
this.style.setProperty('--value-percentage-correct', `${valuePercentage}%`);
} else {
this._correctResponseNumber = null;
}
} else {
this._correctResponseNumber = null;
}
this.requestUpdate();
}

private _updateValue(newValue: number) {
this._value = Math.min(this.max, Math.max(this.min, newValue));
const valuePercentage = ((this._value - this.min) / (this.max - this.min)) * 100;
Expand All @@ -63,7 +81,17 @@ export class QtiSliderInteraction extends LitElement {
<div id="ticks" part="ticks"></div>
<div id="rail" part="rail" @mousedown=${this._onMouseDown} @touchstart=${this._onTouchStart}>
<div id="knob" part="knob"><div id="value" part="value">${this.value}</div></div>
<div id="knob" part="knob">
<div id="value" part="value">${this.value}</div>
</div>
${this._correctResponseNumber !== null
? html`
<div id="knob-correct" part="knob-correct">
<div id="value" part="value">${this._correctResponseNumber}</div>
</div>
`
: null}
</div>
</div>
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import meta, {
SelectPointMultipleNoAreaMapping as SelectPointMultipleNoAreaMappingStory,
GraphicOrder as GraphicOrderStory,
GapMatch as GapMatchStory,
GraphicAssociate as GraphicAssociateStory
GraphicAssociate as GraphicAssociateStory,
Slider as SliderStory
} from './item-show-correct-response.stories';

import '../../../qti-components';
Expand All @@ -28,6 +29,7 @@ const selectPointMultipleNoAreaMappingStory = composeStory(SelectPointMultipleNo
const graphicOrderStory = composeStory(GraphicOrderStory, meta);
const graphicAssociateStory = composeStory(GraphicAssociateStory, meta);
const gapMatchStory = composeStory(GapMatchStory, meta);
const sliderStory = composeStory(SliderStory, meta);

// Helper function to resolve loaders and render story
async function setupStory(story, canvasElement) {
Expand Down Expand Up @@ -96,4 +98,9 @@ describe.sequential('ItemShowCorrectResponse Suite', () => {
await setupStory(GapMatchStory, canvasElement);
await gapMatchStory.play({ canvasElement });
});

test('show correct response - Slider', async () => {
await setupStory(SliderStory, canvasElement);
await sliderStory.play({ canvasElement });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -445,3 +445,54 @@ export const GraphicAssociate: Story = {
});
}
};

export const Slider: Story = {
args: {
'item-url': '/qti-item/example-slider.xml' // Set the new item URL here
},
render: args =>
html` <qti-item>
<div>
<item-container style="display: block;width: 400px; height: 350px;" item-url=${args['item-url']}>
<template>
<style>
qti-assessment-item {
padding: 1rem;
display: block;
aspect-ratio: 4 / 3;
width: 800px;
border: 2px solid blue;
transform: scale(0.5);
transform-origin: top left;
}
</style>
</template>
</item-container>
<item-show-correct-response></item-show-correct-response>
</div>
</qti-item>`,
play: async ({ canvasElement, step }) => {
// wait for qti-simple-choices to be rendered
const canvas = within(canvasElement);
await waitFor(
() => {
const interaction = canvasElement
.querySelector('item-container')
.shadowRoot.querySelector('qti-slider-interaction');
if (!interaction) {
throw new Error('interaction not loaded yet');
}
},
{ timeout: 5000 }
);
const itemContainer = canvasElement.querySelector('item-container');
const interaction = itemContainer.shadowRoot.querySelector('qti-slider-interaction');
const showCorrectButton = canvas.getAllByShadowText(/Show correct/i)[0];
await step('Click on the Show Correct button', async () => {
await showCorrectButton.click();
const correctIndication = interaction.shadowRoot.querySelectorAll('[id="knob-correct"]');
expect(correctIndication.length).toBe(1);
});
}
};

0 comments on commit 57a51f7

Please sign in to comment.