From 57a51f76dc74cc742eb7549f1ca30950c3fd1eff Mon Sep 17 00:00:00 2001 From: Marcel Hoekstra Date: Thu, 13 Feb 2025 16:26:38 +0100 Subject: [PATCH] feat: implement slider interaction with correct response indication and add corresponding story --- public/assets/qti-item/example-slider.xml | 22 ++++++++ .../qti-slider-interaction.styles.ts | 12 +++++ .../qti-slider-interaction.ts | 44 +++++++++++++--- .../item-show-correct-response.spec.ts | 9 +++- .../item-show-correct-response.stories.ts | 51 +++++++++++++++++++ 5 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 public/assets/qti-item/example-slider.xml diff --git a/public/assets/qti-item/example-slider.xml b/public/assets/qti-item/example-slider.xml new file mode 100644 index 00000000..3a709256 --- /dev/null +++ b/public/assets/qti-item/example-slider.xml @@ -0,0 +1,22 @@ + + + + 70 + + + + + + Roughly (to the nearest 10%) what percentage of the Earth's surface is + covered in water? + + + + \ No newline at end of file diff --git a/src/lib/qti-components/qti-interaction/qti-slider-interaction/qti-slider-interaction.styles.ts b/src/lib/qti-components/qti-interaction/qti-slider-interaction/qti-slider-interaction.styles.ts index a93ef48c..0d7ad7c4 100644 --- a/src/lib/qti-components/qti-interaction/qti-slider-interaction/qti-slider-interaction.styles.ts +++ b/src/lib/qti-components/qti-interaction/qti-slider-interaction/qti-slider-interaction.styles.ts @@ -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; diff --git a/src/lib/qti-components/qti-interaction/qti-slider-interaction/qti-slider-interaction.ts b/src/lib/qti-components/qti-interaction/qti-slider-interaction/qti-slider-interaction.ts index bfa2798a..b257aa36 100644 --- a/src/lib/qti-components/qti-interaction/qti-slider-interaction/qti-slider-interaction.ts +++ b/src/lib/qti-components/qti-interaction/qti-slider-interaction/qti-slider-interaction.ts @@ -1,18 +1,20 @@ -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; @@ -20,9 +22,8 @@ export class QtiSliderInteraction extends LitElement { @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() { @@ -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; @@ -63,7 +81,17 @@ export class QtiSliderInteraction extends LitElement {
-
${this.value}
+
+
${this.value}
+
+ + ${this._correctResponseNumber !== null + ? html` +
+
${this._correctResponseNumber}
+
+ ` + : null}
`; diff --git a/src/lib/qti-item/core/components/item-show-correct-response.spec.ts b/src/lib/qti-item/core/components/item-show-correct-response.spec.ts index a18452e3..eb09c109 100644 --- a/src/lib/qti-item/core/components/item-show-correct-response.spec.ts +++ b/src/lib/qti-item/core/components/item-show-correct-response.spec.ts @@ -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'; @@ -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) { @@ -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 }); + }); }); diff --git a/src/lib/qti-item/core/components/item-show-correct-response.stories.ts b/src/lib/qti-item/core/components/item-show-correct-response.stories.ts index 775b1005..43373dcd 100644 --- a/src/lib/qti-item/core/components/item-show-correct-response.stories.ts +++ b/src/lib/qti-item/core/components/item-show-correct-response.stories.ts @@ -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` +
+ + + + +
+
`, + 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); + }); + } +};