@brightspace-ui/labs 2.16.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -83,5 +83,5 @@
83
83
  "@lit/context": "^1.1.3",
84
84
  "lit": "^3"
85
85
  },
86
- "version": "2.16.0"
86
+ "version": "2.18.0"
87
87
  }
@@ -0,0 +1,85 @@
1
+ # @d2l/labs-grade-result
2
+
3
+ A web component used for rendering grades in Brightspace
4
+
5
+ ## Properties
6
+
7
+ #### d2l-labs-d2l-grade-result
8
+
9
+ | Property | Type | Default | Description |
10
+ | ----------------------------------| --------- | ------- | ------------------------------------------------------------ |
11
+ | `href` | `string` | `''` | The Hypermedia route to power the component. This component runs off of the /grade route or an activity. |
12
+ | `token` | `string` | `''` | For authentication |
13
+ | `disableAutoSave` | `boolean` | `false` | Prevent the component from automatically saving the grade to the API when the grade is changed. |
14
+ | `_hideTitle` | `boolean` | `false` | This property will hide the "Overall Grade" title above the component. |
15
+ | `customManualOverrideText` | `string` | `undefined` | This properly will substitute the stock text on the "Manual Override" button. |
16
+ | `customManualOverrideClearText` | `string` | `undefined` | This properly will substitute the stock text on the "Clear Manual Override" button. |
17
+
18
+ ##### Public Methods
19
+
20
+ | Method | Description |
21
+ | ------------------------------ | ------------------------------------------------------------ |
22
+ | `saveGrade(): void` | This is the method used to manually save the grade to the server when `disableAutoSave = true`. This method will emit `@d2l-grade-result-grade-saved-success` or `@d2l-grade-result-grade-saved-error`. |
23
+ | `hasUnsavedChanges(): boolean` | Determines whether the grade has been changed by the user and has not been saved to the server yet. |
24
+
25
+ If you are only interested in rendering the presentational layer of the component, you can simply use the `d2l-grade-result-presentational` component.
26
+
27
+ #### d2l-labs-d2l-grade-result-presentational
28
+
29
+ | Property | GradeType | Type | Default | Description |
30
+ | ----------------------------------| -------------- | --------------------------- | ----------- | ------------------------------------------------------------ |
31
+ | `gradeType` | All | `string ('Numeric' or 'LetterGrade')` | `'Numeric'` | Specifies the type of grade that the component is meant to render. |
32
+ | `labelText` | All | `string` | `''` | The text that appears above the component. |
33
+ | `scoreNumerator` | Numeric | `number` | `0` | The numerator of the numeric score that is given. |
34
+ | `scoreDenominator` | Numeric | `number` | `0` | The denominator of the numeric score that is given. |
35
+ | `selectedLetterGrade` | LetterGrade | `string` | `''` | The current selected letter grade of the options given. |
36
+ | `letterGradeOptions` | LetterGrade | `Object` | `null` | A dictionary where the key is a unique id and the value is an object containing the LetterGrade text and the PercentStart. |
37
+ | `includeGradeButton` | All | `boolean` | `false` | Determines whether the grades icon button is rendered. |
38
+ | `includeReportsButton` | All | `boolean` | `false` | Determines whether the reports icon button is rendered. |
39
+ | `gradeButtonTooltip` | All | `string` | `''` | The text that is inside of the tooltip when hovering over the grades button. |
40
+ | `reportsButtonTooltip` | All | `string` | `''` | The text that is inside of the tooltip when hovering over the reports button. |
41
+ | `readOnly` | All | `boolean` | `false` | Set to `true` if the user does not have permissions to edit the grade. |
42
+ | `isManualOverrideActive` | All | `boolean` | `false` | Set to `true` if the user is currently manually overriding the grade. This will display the button to 'Clear Manual Override'. |
43
+ | `hideTitle` | All | `boolean` | `false` | This property will hide the "Overall Grade" title above the component. |
44
+ | `customManualOverrideClearText` | All | `string` | `undefined` | This property will substitute the stock text on the "Clear Manual Override" button. |
45
+ | `subtitleText` | All | `string` | `undefined` | This property will show the given text under the title. |
46
+ | `required` | Numeric | `Boolean` | `false` | Set to `true` if an undefined/blank grade is not considered valid |
47
+ | `inputLabelText` | Numeric | `string` | `''` | This property sets the label that will be used inside the aria-label and validation error tool-tips |
48
+ | `allowNegativeScore` | Numeric | `boolean` | `'false'` | Set to `true` if negative scores can be entered |
49
+ | `showFlooredScoreWarning` | Numeric | `boolean` | `'false'` | Set to `true` if displaying a negative grade that has been floored at 0 |
50
+
51
+ ## Events
52
+
53
+ #### d2l-labs-d2l-grade-result
54
+
55
+ | Event | Description |
56
+ | ----------------------------------------------- | ------------------------------------------------------------ |
57
+ | `@d2l-grade-result-initialized-success` | This event is fired when the component is successfully initialized and a grade is loaded from the API. |
58
+ | `@d2l-grade-result-initialized-error` | This event is fired when there is an error initializing the component. This is usually caused by an invalid `href` or `token`. |
59
+ | `@d2l-grade-result-grade-updated-success` | This event is fired when the grade is successfully updated on the frontend. |
60
+ | `@d2l-grade-result-grade-updated-error` | This event is fired when there is an error updating the grade on the frontend. |
61
+ | `@d2l-grade-result-grade-saved-success` | This event is fired when the grade is successfully saved to the server. |
62
+ | `@d2l-grade-result-grade-saved-error` | This event is fired when there is an error while saving the grade to the server. |
63
+ | `@d2l-grade-result-grade-button-click` | This event is fired when the grades button is clicked. |
64
+ | `@d2l-grade-result-reports-button-click` | This event is fired when the reports button is clicked. |
65
+ | `@d2l-grade-result-manual-override-clear-click` | This event is fired when the manual override clear is clicked. |
66
+
67
+ #### d2l-labs-d2l-grade-result-presentational
68
+
69
+ | Event | Description |
70
+ | ----------------------------------------------- | ------------------------------------------------------------ |
71
+ | `@d2l-grade-result-grade-button-click` | This event is fired when the grades button is clicked. |
72
+ | `@d2l-grade-result-reports-button-click` | This event is fired when the reports button is clicked. |
73
+ | `@d2l-grade-result-grade-change` | This event is fired on the change of the grade for a `gradeType="Numeric"` grade. |
74
+ | `@d2l-grade-result-letter-score-selected` | This event is fired on the change of the grade for a `gradeType="LetterGrade"` grade. |
75
+ | `@d2l-grade-result-manual-override-clear-click` | This event is fired when the manual override clear is clicked. |
76
+
77
+
78
+ ## Usage
79
+
80
+ ```html
81
+ <script type="module">
82
+ import '@d2l/labs-grade-result/d2l-grade-result.js';
83
+ </script>
84
+ <d2l-labs-d2l-grade-result href="href" token="token" disableAutoSave _hideTitle>My element</d2l-labs-d2l-grade-result>
85
+ ```
@@ -0,0 +1,38 @@
1
+ import '@brightspace-ui/core/components/button/button-icon.js';
2
+ import { html, LitElement } from 'lit';
3
+ import { getUniqueId } from '@brightspace-ui/core/helpers/uniqueId.js';
4
+
5
+ export class D2LGradeResultIconButton extends LitElement {
6
+ static get properties() {
7
+ return {
8
+ text: { type: String },
9
+ icon: { type: String },
10
+ _id: { type: String },
11
+ };
12
+ }
13
+
14
+ constructor() {
15
+ super();
16
+ this._id = getUniqueId();
17
+ }
18
+
19
+ render() {
20
+ return html`
21
+ <d2l-button-icon
22
+ id="d2l-grade-result-icon-button-${this._id}"
23
+ icon="${this.icon}"
24
+ @click="${this._onClick}"
25
+ text="${this.text}"
26
+ ></d2l-button-icon>
27
+ `;
28
+ }
29
+
30
+ _onClick() {
31
+ this.dispatchEvent(new CustomEvent('d2l-grade-result-icon-button-click', {
32
+ bubbles: true,
33
+ composed: true,
34
+ }));
35
+ }
36
+
37
+ }
38
+ customElements.define('d2l-grade-result-icon-button', D2LGradeResultIconButton);
@@ -0,0 +1,91 @@
1
+ import { css, html, LitElement } from 'lit';
2
+ import { bodyStandardStyles } from '@brightspace-ui/core/components/typography/styles.js';
3
+ import { LocalizeLabsElement } from '../localize-labs-element.js';
4
+ import { selectStyles } from '@brightspace-ui/core/components/inputs/input-select-styles.js';
5
+
6
+ export class D2LGradeResultLetterScore extends LocalizeLabsElement(LitElement) {
7
+
8
+ static get properties() {
9
+ return {
10
+ availableOptions: { type: Object },
11
+ label: { type: String },
12
+ selectedOption: { type: String },
13
+ readOnly: { type: Boolean }
14
+ };
15
+ }
16
+
17
+ static get styles() {
18
+ return [selectStyles, bodyStandardStyles, css`
19
+ .d2l-grade-result-letter-score-container {
20
+ width: 8rem;
21
+ }
22
+ .d2l-grade-result-letter-score-select {
23
+ width: 100%;
24
+ }
25
+ .d2l-grade-result-letter-score-score-read-only {
26
+ height: calc(2rem + 2px);
27
+ line-height: calc(2rem + 2px);
28
+ }
29
+ `];
30
+ }
31
+
32
+ constructor() {
33
+ super();
34
+ this.availableOptions = null;
35
+ this.selectedOption = '';
36
+ }
37
+
38
+ render() {
39
+ if (!this.readOnly) {
40
+ return html`
41
+ <div class="d2l-grade-result-letter-score-container">
42
+ <select
43
+ id="d2l-grade"
44
+ aria-label=${this.label ? this.label : this.localize('components:gradeResult:gradeScoreLabel')}
45
+ class="d2l-input-select d2l-grade-result-letter-score-select"
46
+ @change=${this._onOptionSelected}
47
+ .value=${this.selectedOption}>
48
+ ${this._renderOptions()}
49
+ </select>
50
+ </div>
51
+ `;
52
+ } else {
53
+ return html`
54
+ <div class="d2l-grade-result-letter-score-score-read-only">
55
+ <span id="d2l-grade" class="d2l-body-standard">${this._selectedOptionText()}</span>
56
+ </div>
57
+ `;
58
+ }
59
+ }
60
+
61
+ _onOptionSelected(e) {
62
+ this.dispatchEvent(new CustomEvent('d2l-grade-result-letter-score-selected', {
63
+ composed: true,
64
+ bubbles: true,
65
+ detail: {
66
+ value: e.target.value
67
+ }
68
+ }));
69
+ }
70
+
71
+ _renderOptions() {
72
+ const itemTemplate = [];
73
+ for (const [id, option] of Object.entries(this.availableOptions)) {
74
+ if (this.selectedOption === id) {
75
+ itemTemplate.push(html`<option selected value=${id}>${option.LetterGrade}</option>`);
76
+ } else {
77
+ itemTemplate.push(html`<option value=${id}>${option.LetterGrade}</option>`);
78
+ }
79
+ }
80
+ return itemTemplate;
81
+ }
82
+
83
+ _selectedOptionText() {
84
+ if (this.availableOptions[this.selectedOption]) {
85
+ return this.availableOptions[this.selectedOption].LetterGrade;
86
+ }
87
+ }
88
+
89
+ }
90
+
91
+ customElements.define('d2l-grade-result-letter-score', D2LGradeResultLetterScore);
@@ -0,0 +1,108 @@
1
+ import '@brightspace-ui/core/components/inputs/input-number.js';
2
+ import '@brightspace-ui/core/components/offscreen/offscreen.js';
3
+ import { bodyCompactStyles, bodyStandardStyles } from '@brightspace-ui/core/components/typography/styles.js';
4
+ import { css, html, LitElement, nothing } from 'lit';
5
+ import { LocalizeLabsElement } from '../localize-labs-element.js';
6
+
7
+ const numberConverter = {
8
+ fromAttribute: (attr) => { return !attr ? undefined : Number(attr); },
9
+ toAttribute: (prop) => { return String(prop); }
10
+ };
11
+
12
+ const EXTRA_SPACE = 2.5;
13
+ const MIN_WIDTH = 5.5;
14
+ const MIN_NEGATIVE_GRADE = -9999999999;
15
+ const MIN_POSITIVE_GRADE = 0;
16
+
17
+ export class D2LGradeResultNumericScore extends LocalizeLabsElement(LitElement) {
18
+ static get properties() {
19
+ return {
20
+ label: { type: String },
21
+ scoreNumerator: { type: Number, converter: numberConverter },
22
+ scoreDenominator: { type: Number },
23
+ readOnly: { type: Boolean },
24
+ required: { type: Boolean },
25
+ allowNegativeScore: { type: Boolean },
26
+ showFlooredScoreWarning: { type: Boolean },
27
+ };
28
+ }
29
+
30
+ static get styles() {
31
+ return [bodyCompactStyles, bodyStandardStyles, css`
32
+ .d2l-grade-result-numeric-score-container {
33
+ align-items: center;
34
+ display: flex;
35
+ flex-direction: row;
36
+ }
37
+ .d2l-grade-result-numeric-score-score-read-only {
38
+ height: calc(2rem + 2px);
39
+ line-height: calc(2rem + 2px);
40
+ max-width: 5.25rem;
41
+ }
42
+ .d2l-grade-result-numeric-score-hint {
43
+ margin: 0 0.3rem;
44
+ }
45
+ `];
46
+ }
47
+
48
+ render() {
49
+ const roundedNumerator = isNaN(this.scoreNumerator) ? '' : Math.round((this.scoreNumerator + Number.EPSILON) * 100) / 100;
50
+
51
+ const denominatorLength = isNaN(this.scoreDenominator) ? 0 : this.scoreDenominator.toString().length;
52
+ const numeratorLength = roundedNumerator.toString().length;
53
+ const dynamicWidth = numeratorLength <= denominatorLength ? denominatorLength + EXTRA_SPACE : (numeratorLength * 0.5) + (denominatorLength * 0.5) + EXTRA_SPACE;
54
+
55
+ return html`
56
+ <div class="d2l-grade-result-numeric-score-container">
57
+ ${!this.readOnly ? html`
58
+ <div class="d2l-grade-result-numeric-score-score">
59
+ <d2l-form>
60
+ <d2l-input-number
61
+ ?required=${this.required}
62
+ id="d2l-grade"
63
+ label=${this.label ? this.label : this.localize('components:gradeResult:gradeScoreLabel')}
64
+ label-hidden
65
+ value="${this.scoreNumerator}"
66
+ input-width="${dynamicWidth > MIN_WIDTH ? dynamicWidth : MIN_WIDTH}rem"
67
+ min="${this.allowNegativeScore ? MIN_NEGATIVE_GRADE : MIN_POSITIVE_GRADE}"
68
+ max="9999999999"
69
+ max-fraction-digits="2"
70
+ unit="/ ${this.scoreDenominator}"
71
+ unit-label=${this.localize('components:gradeResult:outOfDenominator', { denominator: this.scoreDenominator })}
72
+ value-align="end"
73
+ @change=${this._onGradeChange}
74
+ ></d2l-input-number>
75
+ </d2l-form>
76
+ </div>
77
+ ` : html`
78
+ <div
79
+ aria-hidden="true"
80
+ class="d2l-body-standard d2l-grade-result-numeric-score-score-read-only"
81
+ id="d2l-grade">
82
+ ${roundedNumerator} / ${this.scoreDenominator}
83
+ </div>
84
+ <d2l-offscreen>${this.localize('components:gradeResult:numeratorOutOfDenominator', { numerator: roundedNumerator, denominator: this.scoreDenominator })}</d2l-offscreen>
85
+ `}
86
+ ${this.showFlooredScoreWarning ? html`
87
+ <div class="d2l-grade-result-numeric-score-hint d2l-body-compact">
88
+ ${this.localize('components:gradeResult:cannotBeNegative')}
89
+ </div>
90
+ ` : nothing}
91
+ </div>
92
+ `;
93
+ }
94
+
95
+ _onGradeChange(e) {
96
+ const newScore = e.target.value;
97
+ this.dispatchEvent(new CustomEvent('d2l-grade-result-grade-change', {
98
+ bubbles: true,
99
+ composed: true,
100
+ detail: {
101
+ value: newScore
102
+ }
103
+ }));
104
+ }
105
+
106
+ }
107
+
108
+ customElements.define('d2l-grade-result-numeric-score', D2LGradeResultNumericScore);
@@ -0,0 +1,253 @@
1
+ import './grade-result-icon-button.js';
2
+ import './grade-result-numeric-score.js';
3
+ import './grade-result-letter-score.js';
4
+ import './grade-result-student-grade-preview.js';
5
+ import '@brightspace-ui/core/components/button/button-subtle.js';
6
+ import { bodySmallStyles, labelStyles } from '@brightspace-ui/core/components/typography/styles.js';
7
+ import { css, html, LitElement, nothing } from 'lit';
8
+ import { GradeType } from '../../controllers/grade-result/Grade.js';
9
+ import { ifDefined } from 'lit/directives/if-defined.js';
10
+ import { LocalizeLabsElement } from '../localize-labs-element.js';
11
+
12
+ const numberConverter = {
13
+ fromAttribute: (attr) => { return !attr ? undefined : Number(attr); },
14
+ toAttribute: (prop) => { return String(prop); }
15
+ };
16
+
17
+ export class D2LGradeResultPresentational extends LocalizeLabsElement(LitElement) {
18
+ static get properties() {
19
+ return {
20
+ allowNegativeScore: { type: Boolean },
21
+ customManualOverrideClearText: { type: String },
22
+ displayStudentGradePreview: { type: Boolean, attribute: 'display-student-grade-preview' },
23
+ gradeButtonTooltip: { type: String },
24
+ gradeType: { type: String },
25
+ hideTitle: { type: Boolean },
26
+ includeGradeButton: { type: Boolean },
27
+ includeReportsButton: { type: Boolean },
28
+ inputLabelText: { type: String },
29
+ isManualOverrideActive: { type: Boolean },
30
+ labelHeadingLevel: { type: Number },
31
+ labelText: { type: String },
32
+ letterGradeOptions: { type: Object },
33
+ readOnly: { type: Boolean },
34
+ reportsButtonTooltip: { type: String },
35
+ required: { type: Boolean },
36
+ scoreDenominator: { type: Number },
37
+ scoreNumerator: { type: Number, converter: numberConverter },
38
+ selectedLetterGrade: { type: String },
39
+ showFlooredScoreWarning: { type: Boolean },
40
+ subtitleText: { type: String },
41
+ studentGradePreview: { type: Object, attribute: 'student-grade-preview' },
42
+ };
43
+ }
44
+
45
+ static get styles() {
46
+ return [ bodySmallStyles, labelStyles, css`
47
+ .d2l-grade-result-presentational-container {
48
+ display: flex;
49
+ flex-wrap: wrap;
50
+ gap: 0 0.9rem;
51
+ }
52
+ .d2l-grade-result-presentational-score-container {
53
+ display: flex;
54
+ flex-wrap: wrap;
55
+ gap: 0.3rem;
56
+ }
57
+ .d2l-grade-result-manual-override-clear {
58
+ margin-top: 0.3rem;
59
+ }
60
+ .d2l-label-text {
61
+ line-height: 1.6rem;
62
+ margin-bottom: 0.4rem;
63
+ }
64
+ .d2l-grade-result-presentational-subtitle {
65
+ font-weight: bold;
66
+ margin-top: -4px;
67
+ }
68
+ `];
69
+ }
70
+
71
+ constructor() {
72
+ super();
73
+ this.allowNegativeScore = false;
74
+ this.customManualOverrideClearText = undefined;
75
+ this.hideTitle = false;
76
+ this.includeGradeButton = false;
77
+ this.includeReportsButton = false;
78
+ this.isManualOverrideActive = false;
79
+ this.labelHeadingLevel = undefined;
80
+ this.readOnly = false;
81
+ this.selectedLetterGrade = '';
82
+ this.showFlooredScoreWarning = false;
83
+ this.subtitleText = undefined;
84
+ }
85
+
86
+ render() {
87
+ return html`
88
+ <div class="d2l-grade-result-presentational-container">
89
+ <div>
90
+ ${this._renderScoreLabel()}
91
+ ${this._renderScoreSubtitle()}
92
+ <div class="d2l-grade-result-presentational-score-container">
93
+ ${this._renderScoreComponent()}
94
+ ${this._renderGradeIconButton()}
95
+ ${this._renderGradeReportIconButton()}
96
+ </div>
97
+ </div>
98
+ ${this._renderStudentGradePreview()}
99
+ </div>
100
+ ${this._renderManualOverrideButtonComponent()}
101
+ `;
102
+ }
103
+
104
+ _isReadOnly() {
105
+ return Boolean(this.readOnly);
106
+ }
107
+
108
+ _onGradeButtonClick() {
109
+ this.dispatchEvent(new CustomEvent('d2l-grade-result-grade-button-click', {
110
+ bubbles: true,
111
+ composed: true,
112
+ }));
113
+ }
114
+
115
+ _onManualOverrideClearClick() {
116
+ this.dispatchEvent(new CustomEvent('d2l-grade-result-manual-override-clear-click', {
117
+ bubbles: true,
118
+ composed: true
119
+ }));
120
+ }
121
+
122
+ _onReportsButtonClick() {
123
+ this.dispatchEvent(new CustomEvent('d2l-grade-result-reports-button-click', {
124
+ bubbles: true,
125
+ composed: true,
126
+ }));
127
+ }
128
+
129
+ _renderGradeIconButton() {
130
+ if (!this.includeGradeButton) {
131
+ return nothing;
132
+ }
133
+
134
+ return html`
135
+ <d2l-grade-result-icon-button
136
+ icon="tier1:grade"
137
+ text=${this.gradeButtonTooltip}
138
+ @d2l-grade-result-icon-button-click=${this._onGradeButtonClick}
139
+ ></d2l-grade-result-icon-button>
140
+ `;
141
+ }
142
+
143
+ _renderGradeReportIconButton() {
144
+ if (!this.includeReportsButton) {
145
+ return nothing;
146
+ }
147
+
148
+ return html`
149
+ <d2l-grade-result-icon-button
150
+ icon="tier1:reports"
151
+ text=${this.reportsButtonTooltip}
152
+ @d2l-grade-result-icon-button-click=${this._onReportsButtonClick}
153
+ ></d2l-grade-result-icon-button>
154
+ `;
155
+ }
156
+
157
+ _renderLetterScoreComponent() {
158
+ return html`
159
+ <d2l-grade-result-letter-score
160
+ .availableOptions=${this.letterGradeOptions}
161
+ .label=${this.inputLabelText}
162
+ .readOnly=${this._isReadOnly()}
163
+ .selectedOption=${this.selectedLetterGrade}
164
+ ></d2l-grade-result-letter-score>
165
+ `;
166
+ }
167
+
168
+ _renderManualOverrideButtonComponent() {
169
+ if (!this.isManualOverrideActive) {
170
+ return nothing;
171
+ }
172
+
173
+ const text = this.customManualOverrideClearText ? this.customManualOverrideClearText : this.localize('components:gradeResult:clearManualOverride');
174
+
175
+ return html`
176
+ <d2l-button-subtle
177
+ class="d2l-grade-result-manual-override-clear"
178
+ icon="tier1:close-default"
179
+ text=${text}
180
+ @click=${this._onManualOverrideClearClick}
181
+ ></d2l-button-subtle>
182
+ `;
183
+ }
184
+
185
+ _renderNumericScoreComponent() {
186
+ return html`
187
+ <d2l-grade-result-numeric-score
188
+ ?allowNegativeScore=${this.allowNegativeScore}
189
+ .label=${this.inputLabelText}
190
+ .readOnly=${this._isReadOnly()}
191
+ ?required=${this.required}
192
+ .scoreDenominator=${this.scoreDenominator}
193
+ .scoreNumerator=${this.scoreNumerator}
194
+ ?showFlooredScoreWarning=${this.showFlooredScoreWarning}
195
+ ></d2l-grade-result-numeric-score>
196
+ `;
197
+ }
198
+
199
+ _renderScoreComponent() {
200
+ if (this.gradeType === GradeType.Number) {
201
+ return this._renderNumericScoreComponent();
202
+ } else if (this.gradeType === GradeType.Letter) {
203
+ return this._renderLetterScoreComponent();
204
+ } else {
205
+ throw new Error('INVALID GRADE TYPE PROVIDED');
206
+ }
207
+ }
208
+
209
+ _renderScoreLabel() {
210
+ if (this.hideTitle || !this.labelText) {
211
+ return nothing;
212
+ }
213
+
214
+ return html`
215
+ <label
216
+ aria-level=${ifDefined(this.labelHeadingLevel)}
217
+ class="d2l-label-text"
218
+ for="d2l-grade"
219
+ role=${this.labelHeadingLevel ? 'heading' : ''}>
220
+ ${this.labelText}
221
+ </label>
222
+ `;
223
+ }
224
+
225
+ _renderScoreSubtitle() {
226
+ if (!this.subtitleText) {
227
+ return nothing;
228
+ }
229
+
230
+ return html`
231
+ <div class="d2l-grade-result-presentational-subtitle d2l-body-small">
232
+ ${this.subtitleText}
233
+ </div>
234
+ `;
235
+ }
236
+
237
+ _renderStudentGradePreview() {
238
+ if (!this.studentGradePreview) {
239
+ return nothing;
240
+ }
241
+
242
+ return html`
243
+ <d2l-grade-result-student-grade-preview
244
+ ?hidden=${!this.displayStudentGradePreview}
245
+ out-of=${this.scoreDenominator}
246
+ .studentGradePreview=${this.studentGradePreview}
247
+ ></d2l-grade-result-student-grade-preview>
248
+ `;
249
+ }
250
+
251
+ }
252
+
253
+ customElements.define('d2l-labs-d2l-grade-result-presentational', D2LGradeResultPresentational);
@@ -0,0 +1,139 @@
1
+ import '@brightspace-ui/core/components/offscreen/offscreen.js';
2
+ import { bodyCompactStyles, bodySmallStyles, labelStyles } from '@brightspace-ui/core/components/typography/styles.js';
3
+ import { css, html, LitElement, nothing } from 'lit';
4
+ import { formatNumber } from '@brightspace-ui/intl/lib/number.js';
5
+ import { LocalizeLabsElement } from '../localize-labs-element.js';
6
+
7
+ const previewOptions = {
8
+ colour: 'colour',
9
+ score: 'score',
10
+ symbol: 'symbol'
11
+ };
12
+
13
+ export class D2LGradeResultStudentGradePreview extends LocalizeLabsElement(LitElement) {
14
+
15
+ static get properties() {
16
+ return {
17
+ hideLabel: {
18
+ type: Boolean,
19
+ attribute: 'hide-label'
20
+ },
21
+ outOf: {
22
+ type: Number,
23
+ attribute: 'out-of'
24
+ },
25
+ studentGradePreview: {
26
+ type: Object,
27
+ attribute: 'student-grade-preview'
28
+ }
29
+ };
30
+ }
31
+
32
+ static get styles() {
33
+ return [bodySmallStyles, bodyCompactStyles, labelStyles, css`
34
+ :host {
35
+ display: inline-block;
36
+ }
37
+ :host([hidden]) {
38
+ display: none;
39
+ }
40
+ .d2l-grade-result-student-grade-preview-container {
41
+ align-items: center;
42
+ display: flex;
43
+ flex-direction: row;
44
+ gap: 0.5rem;
45
+ min-height: calc(2rem + 2px);
46
+ }
47
+ .d2l-grade-result-student-grade-preview-colour {
48
+ border-radius: 6px;
49
+ height: 0.9rem;
50
+ width: 0.9rem;
51
+ }
52
+ .d2l-label-text {
53
+ line-height: 1.6rem;
54
+ margin-bottom: 0.4rem;
55
+ }
56
+ `];
57
+ }
58
+
59
+ constructor() {
60
+ super();
61
+ this.hideLabel = false;
62
+ }
63
+
64
+ render() {
65
+ if (!this.studentGradePreview) {
66
+ return nothing;
67
+ }
68
+
69
+ let label = nothing;
70
+ if (!this.hideLabel) {
71
+ label = html`
72
+ <label class="d2l-label-text d2l-skeletize" for="d2l-grade-result-student-grade-preview">
73
+ ${this.localize('components:gradeResult:studentGradePreviewLabel')}
74
+ </label>
75
+ `;
76
+ }
77
+
78
+ const shouldDisplayAny = Object.values(previewOptions).some(option => this._shouldDisplay(option));
79
+ if (!shouldDisplayAny) {
80
+ return html`
81
+ ${label}
82
+ <div class="d2l-body-small d2l-grade-result-student-grade-preview-container" id="d2l-grade-result-student-grade-preview">
83
+ ${this.localize('components:gradeResult:studentGradePreviewNotShown')}
84
+ </div>
85
+ `;
86
+ }
87
+
88
+ return html`
89
+ ${label}
90
+ <div id="d2l-grade-result-student-grade-preview" class="d2l-grade-result-student-grade-preview-container">
91
+ ${this._renderColour()}
92
+ ${this._renderScoreAndSymbol()}
93
+ </div>
94
+ `;
95
+ }
96
+
97
+ _renderColour() {
98
+ if (!this._shouldDisplay(previewOptions.colour) || !this.studentGradePreview.colour) {
99
+ return nothing;
100
+ }
101
+
102
+ return html`
103
+ <div class="d2l-grade-result-student-grade-preview-colour"
104
+ style="background-color: ${this.studentGradePreview.colour};">
105
+ </div>
106
+ `;
107
+ }
108
+
109
+ _renderScoreAndSymbol() {
110
+ if (!this._shouldDisplay(previewOptions.score) && !this._shouldDisplay(previewOptions.symbol)) {
111
+ return nothing;
112
+ }
113
+
114
+ const score = this._shouldDisplay(previewOptions.score)
115
+ ? `${this.studentGradePreview?.score && typeof this.studentGradePreview?.score === 'number' ? formatNumber(this.studentGradePreview?.score) : ''} / ${this.outOf && typeof this.outOf === 'number' ? formatNumber(this.outOf) : 0}`
116
+ : '';
117
+ const accessibleScore = this._shouldDisplay(previewOptions.score) ? this.localize('components:gradeResult:numeratorOutOfDenominator', { numerator: this.studentGradePreview?.score, denominator: this.outOf }) : '';
118
+
119
+ const symbol = this._shouldDisplay(previewOptions.symbol) ? this.studentGradePreview?.symbol : '';
120
+
121
+ const separator = score && symbol ? ' - ' : '';
122
+
123
+ return html`
124
+ <div aria-hidden="true" class="d2l-body-compact">
125
+ ${`${score}${separator}${symbol}`}
126
+ </div>
127
+ <d2l-offscreen>
128
+ ${`${accessibleScore}${separator}${symbol}`}
129
+ </d2l-offscreen>
130
+ `;
131
+ }
132
+
133
+ _shouldDisplay(property) {
134
+ return Object.prototype.hasOwnProperty.call(this.studentGradePreview, property);
135
+ }
136
+
137
+ }
138
+
139
+ customElements.define('d2l-grade-result-student-grade-preview', D2LGradeResultStudentGradePreview);
@@ -0,0 +1,175 @@
1
+
2
+ export const GradeType = {
3
+ Letter: 'LetterGrade',
4
+ Number: 'Numeric'
5
+ };
6
+
7
+ export const GradeErrors = {
8
+ GET_LETTER_GRADE_FROM_NUMERIC_SCORE: 'Grade must be of type LetterGrade to get the letter grade',
9
+ GET_ASSIGNED_VALUE_FROM_NUMERIC_SCORE: 'Grade must be of type LetterGrade to get the assigned value',
10
+ INVALID_SCORE_TYPE: 'Invalid scoreType provided',
11
+ INVALID_SCORE: 'Invalid score provided',
12
+ INVALID_OUT_OF: 'Invalid outOf provided',
13
+ INVALID_LETTER_GRADE: 'Invalid letterGrade provided',
14
+ INVALID_LETTER_GRADE_ID: 'Invalid letterGradeId provided',
15
+ INVALID_LETTER_GRADE_OPTIONS: 'Invalid letterGradeOptions provided',
16
+ LETTER_GRADE_NOT_IN_OPTIONS: 'letterGrade must be one of the letterGradeOptions provided',
17
+ LETTER_GRADE_ID_NO_ASSIGNED_VALUE: 'LetterGradeId does not have a corresponding AssignedValue',
18
+ };
19
+
20
+ export class Grade {
21
+
22
+ constructor(scoreType, score, outOf, letterGrade, letterGradeOptions, entity, calculatedScore = null, aggregatedScore = null, display = null) {
23
+ this.entity = entity;
24
+ this.isManuallyOverridden = false;
25
+ this.calculatedScore = calculatedScore;
26
+ this.aggregatedScore = aggregatedScore;
27
+ this.display = display;
28
+ this.outOf = outOf;
29
+ this.scoreType = this._parseScoreType(scoreType);
30
+ if (this.isNumberGrade()) {
31
+ this._parseNumberGrade(score);
32
+ } else {
33
+ const letterGradeId = this._getLetterGradeIdFromLetterGrade(letterGrade, letterGradeOptions);
34
+ this._parseLetterGrade(letterGradeId, letterGradeOptions);
35
+ }
36
+ }
37
+
38
+ getDisplay() {
39
+ return this.display;
40
+ }
41
+
42
+ getEntity() {
43
+ return this.entity;
44
+ }
45
+
46
+ getLetterGrade() {
47
+ if (this.isNumberGrade()) {
48
+ throw new Error(GradeErrors.GET_LETTER_GRADE_FROM_NUMERIC_SCORE);
49
+ }
50
+ return this.letterGrade;
51
+ }
52
+
53
+ getLetterGradeAssignedValue() {
54
+ if (this.isNumberGrade()) {
55
+ throw new Error(GradeErrors.GET_LETTER_GRADE_FROM_NUMERIC_SCORE);
56
+ }
57
+
58
+ const letterGradeOption = this.letterGradeOptions[this.letterGradeId];
59
+
60
+ if (!letterGradeOption || (typeof letterGradeOption.AssignedValue !== 'number' && letterGradeOption.AssignedValue !== null)) {
61
+ throw new Error(GradeErrors.LETTER_GRADE_ID_NO_ASSIGNED_VALUE);
62
+ }
63
+
64
+ return letterGradeOption.AssignedValue;
65
+ }
66
+
67
+ getLetterGradeOptions() {
68
+ return this.letterGradeOptions;
69
+ }
70
+
71
+ getOutOf() {
72
+ return this.outOf;
73
+ }
74
+
75
+ getScore() {
76
+ return this.isNumberGrade() ? this.score : this.letterGradeId;
77
+ }
78
+
79
+ getScoreType() {
80
+ return this.scoreType;
81
+ }
82
+
83
+ isLetterGrade() {
84
+ return this.scoreType === GradeType.Letter;
85
+ }
86
+
87
+ isNumberGrade() {
88
+ return this.scoreType === GradeType.Number;
89
+ }
90
+
91
+ setScore(score) {
92
+ if (this.isNumberGrade()) {
93
+ this._parseNumberGrade(score, this.outOf);
94
+ } else {
95
+ this._parseLetterGrade(score, this.letterGradeOptions);
96
+ }
97
+ }
98
+
99
+ _getLetterGradeIdFromLetterGrade(letterGrade, letterGradeOptions) {
100
+ if ((!letterGrade || typeof letterGrade !== 'string') && letterGrade !== null && letterGrade !== '') {
101
+ throw new Error(GradeErrors.INVALID_LETTER_GRADE);
102
+ }
103
+ if (!letterGradeOptions || typeof letterGradeOptions !== 'object' || Object.keys(letterGradeOptions).length === 0) {
104
+ throw new Error(GradeErrors.INVALID_LETTER_GRADE_OPTIONS);
105
+ }
106
+
107
+ let letterGradeId;
108
+
109
+ // this is the "None" case which has the id 0
110
+ if (letterGrade === '' || letterGrade === null) {
111
+ letterGradeId = '0';
112
+ } else {
113
+ letterGradeId = Object.keys(letterGradeOptions).find(key =>
114
+ letterGradeOptions[key].LetterGrade === letterGrade
115
+ );
116
+ }
117
+
118
+ if (letterGradeId === undefined) {
119
+ throw new Error(GradeErrors.LETTER_GRADE_NOT_IN_OPTIONS);
120
+ }
121
+
122
+ return letterGradeId;
123
+ }
124
+
125
+ _parseLetterGrade(letterGradeId, letterGradeOptions) {
126
+ if (!letterGradeId && letterGradeId !== 0) {
127
+ throw new Error(GradeErrors.INVALID_LETTER_GRADE_ID);
128
+ }
129
+
130
+ if (!letterGradeOptions || Object.keys(letterGradeOptions).length === 0) {
131
+ throw new Error(GradeErrors.INVALID_LETTER_GRADE_OPTIONS);
132
+ }
133
+
134
+ this.score = null;
135
+ this.letterGradeId = letterGradeId;
136
+ this.letterGrade = letterGradeOptions[letterGradeId].LetterGrade;
137
+ this.letterGradeOptions = letterGradeOptions;
138
+ }
139
+
140
+ _parseNumberGrade(score) {
141
+ if (score === undefined) {
142
+ score = '';
143
+ } else if (isNaN(score)) {
144
+ throw new Error(GradeErrors.INVALID_SCORE);
145
+ } else if (typeof score === 'string') {
146
+ score = Number(score);
147
+ }
148
+
149
+ if (this.calculatedScore !== null) {
150
+ this.isManuallyOverridden = score !== this.calculatedScore;
151
+ }
152
+
153
+ this.score = score;
154
+ this.letterGradeId = null;
155
+ this.letterGrade = null;
156
+ this.letterGradeOptions = null;
157
+ }
158
+
159
+ _parseScoreType(scoreType) {
160
+ const invalidScoreError = new Error(GradeErrors.INVALID_SCORE_TYPE);
161
+
162
+ if (!scoreType || typeof scoreType !== 'string') {
163
+ throw invalidScoreError;
164
+ }
165
+
166
+ if (scoreType.toLowerCase() === GradeType.Number.toLowerCase()) {
167
+ return GradeType.Number;
168
+ } else if (scoreType.toLowerCase() === GradeType.Letter.toLowerCase()) {
169
+ return GradeType.Letter;
170
+ } else {
171
+ throw invalidScoreError;
172
+ }
173
+ }
174
+
175
+ }