@brightspace-ui/core 3.188.0 → 3.189.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.
@@ -59,6 +59,15 @@
59
59
  </d2l-input-radio-group>
60
60
  </d2l-demo-snippet>
61
61
 
62
+ <h2>Group with disabled item(with disabled tooltip)</h2>
63
+ <d2l-demo-snippet>
64
+ <d2l-input-radio-group label="Bread">
65
+ <d2l-input-radio label="Whole wheat" checked></d2l-input-radio>
66
+ <d2l-input-radio label="Baguette" disabled disabled-tooltip="This option is currently unavailable."></d2l-input-radio>
67
+ <d2l-input-radio label="Marble Rye"></d2l-input-radio>
68
+ </d2l-input-radio-group>
69
+ </d2l-demo-snippet>
70
+
62
71
  <h2>Inline help</h2>
63
72
  <d2l-demo-snippet>
64
73
  <d2l-input-radio-group label="Bread">
@@ -72,7 +72,8 @@ class InputRadioGroup extends PropertyRequiredMixin(SkeletonMixin(FormElementMix
72
72
  @click="${this.#handleClick}"
73
73
  @d2l-input-radio-checked="${this.#handleRadioChecked}"
74
74
  @keydown="${this.#handleKeyDown}"
75
- role="radiogroup">
75
+ @focusout="${this.#handleFocusout}"
76
+ role="radiogroup">
76
77
  <slot @slotchange="${this.#handleSlotChange}"></slot>
77
78
  </div>
78
79
  `;
@@ -90,29 +91,22 @@ class InputRadioGroup extends PropertyRequiredMixin(SkeletonMixin(FormElementMix
90
91
  }
91
92
 
92
93
  focus() {
93
- const radios = this.#getRadios();
94
- if (radios.length === 0) return;
95
- let firstFocusable = null;
96
- let firstChecked = null;
97
- radios.forEach(el => {
98
- if (firstFocusable === null && !el.disabled) firstFocusable = el;
99
- if (firstChecked === null && el._checked) firstChecked = el;
100
- });
101
- const focusElem = firstChecked || firstFocusable;
94
+ const focusElem = this.#getFirstFocusableRadio();
95
+ if (!focusElem) return;
102
96
  focusElem.focus();
103
97
  setTimeout(() => focusElem.focus()); // timeout required when following link from form validation
104
98
  }
105
99
 
106
100
  #labelId = getUniqueId();
107
101
 
108
- async #doUpdateChecked(newChecked, doFocus, doDispatchEvent) {
102
+ async #doUpdateChecked(newChecked, doDispatchEvent) {
103
+ if (newChecked._checked || newChecked.disabled) return;
104
+
109
105
  const radios = this.#getRadios();
110
106
  let prevChecked = null;
111
107
  radios.forEach(el => {
112
108
  if (el._checked) prevChecked = el;
113
109
  });
114
- if (prevChecked === newChecked) return;
115
-
116
110
  newChecked._checked = true;
117
111
  if (prevChecked !== null) {
118
112
  prevChecked._checked = false;
@@ -130,10 +124,34 @@ class InputRadioGroup extends PropertyRequiredMixin(SkeletonMixin(FormElementMix
130
124
  }));
131
125
  }
132
126
 
133
- if (doFocus) {
134
- await newChecked.updateComplete; // wait for tabindex to be updated
135
- newChecked.focus();
127
+ }
128
+
129
+ async #focusOption(option) {
130
+ this.#doUpdateChecked(option, true);
131
+ const active = this.#getActiveRadio();
132
+ if (active === option) return;
133
+ option._focusable = true;
134
+ await option.updateComplete;
135
+ option.focus();
136
+ if (active) active._focusable = false;
137
+ }
138
+
139
+ #getActiveRadio() {
140
+ const activeElem = this.getRootNode().activeElement;
141
+ if (activeElem?.tagName === 'D2L-INPUT-RADIO' && this.contains(activeElem)) {
142
+ return activeElem;
143
+ }
144
+ return null;
145
+ }
146
+
147
+ #getFirstFocusableRadio() {
148
+ let firstFocusable = null;
149
+ for (const radio of this.#getRadios()) {
150
+ if (radio.focusDisabled) continue;
151
+ if (radio._checked) return radio;
152
+ if (!firstFocusable) firstFocusable = radio;
136
153
  }
154
+ return firstFocusable;
137
155
  }
138
156
 
139
157
  #getRadios() {
@@ -144,9 +162,13 @@ class InputRadioGroup extends PropertyRequiredMixin(SkeletonMixin(FormElementMix
144
162
 
145
163
  #handleClick(e) {
146
164
  if (e.target.tagName !== 'D2L-INPUT-RADIO') return;
147
- if (e.target.disabled) return;
148
- this.#doUpdateChecked(e.target, true, true);
149
- e.preventDefault();
165
+ this.#focusOption(e.target);
166
+ if (!e.target.disabled) e.preventDefault();
167
+ }
168
+
169
+ #handleFocusout(e) {
170
+ if (this.contains(e.relatedTarget)) return;
171
+ this.#recalculateState(false);
150
172
  }
151
173
 
152
174
  #handleKeyDown(e) {
@@ -164,27 +186,19 @@ class InputRadioGroup extends PropertyRequiredMixin(SkeletonMixin(FormElementMix
164
186
 
165
187
  if (newOffset === null) return;
166
188
 
167
- const radios = this.#getRadios().filter(el => !el.disabled || el._checked);
168
- let checkedIndex = -1;
169
- let firstFocusableIndex = -1;
170
- radios.forEach((el, i) => {
171
- if (el._checked) checkedIndex = i;
172
- if (firstFocusableIndex < 0 && !el.disabled) firstFocusableIndex = i;
173
- });
174
- if (checkedIndex === -1) {
175
- if (firstFocusableIndex === -1) return;
176
- checkedIndex = firstFocusableIndex;
177
- }
189
+ const radios = this.#getRadios().filter(el => !el.focusDisabled);
190
+ const activeRadio = this.#getActiveRadio();
191
+ const currentIndex = radios.findIndex(el => el === activeRadio);
178
192
 
179
- const newIndex = (checkedIndex + newOffset + radios.length) % radios.length;
180
- this.#doUpdateChecked(radios[newIndex], true, true);
193
+ const newIndex = (currentIndex + newOffset + radios.length) % radios.length;
194
+ this.#focusOption(radios[newIndex]);
181
195
 
182
196
  e.preventDefault();
183
197
  }
184
198
 
185
199
  #handleRadioChecked(e) {
186
200
  if (e.detail.checked) {
187
- this.#doUpdateChecked(e.target, false, false);
201
+ this.#doUpdateChecked(e.target, false);
188
202
  } else {
189
203
  e.target._checked = false;
190
204
  this.#recalculateState(true);
@@ -195,24 +209,17 @@ class InputRadioGroup extends PropertyRequiredMixin(SkeletonMixin(FormElementMix
195
209
  this.#recalculateState(false);
196
210
  }
197
211
 
198
- #recalculateState(doValidate) {
212
+ #recalculateState(doValidate = false) {
199
213
  const radios = this.#getRadios();
200
214
  if (radios.length === 0) return;
201
215
 
202
- let firstFocusable = null;
203
216
  const checkedRadios = [];
204
217
  radios.forEach(el => {
205
- if (firstFocusable === null && !el.disabled) firstFocusable = el;
206
218
  if (el._checked) checkedRadios.push(el);
207
219
  el._isInitFromGroup = true;
208
- el._firstFocusable = false;
220
+ el._focusable = false;
209
221
  });
210
222
 
211
- // let the first non-disabled radio know it's first so it can be focusable
212
- if (checkedRadios.length === 0 && firstFocusable !== null) {
213
- firstFocusable._firstFocusable = true;
214
- }
215
-
216
223
  // only the last checked radio is actually checked
217
224
  for (let i = 0; i < checkedRadios.length - 1; i++) {
218
225
  checkedRadios[i]._checked = false;
@@ -220,11 +227,15 @@ class InputRadioGroup extends PropertyRequiredMixin(SkeletonMixin(FormElementMix
220
227
  if (checkedRadios.length > 0) {
221
228
  const lastCheckedRadio = checkedRadios[checkedRadios.length - 1];
222
229
  lastCheckedRadio._checked = true;
230
+ lastCheckedRadio._focusable = true;
223
231
  this.setFormValue(lastCheckedRadio.value);
224
232
  if (this.required) {
225
233
  this.setValidity({ valueMissing: false });
226
234
  }
227
235
  } else {
236
+ // let the first non-focus-disabled radio know it's first so it can be focusable
237
+ const firstFocusable = this.#getFirstFocusableRadio();
238
+ if (firstFocusable) firstFocusable._focusable = true;
228
239
  this.setFormValue('');
229
240
  if (this.required) {
230
241
  this.setValidity({ valueMissing: true });
@@ -70,13 +70,23 @@ export const radioStyles = css`
70
70
  padding-inline-start: 1.7rem;
71
71
  vertical-align: middle;
72
72
  }
73
- .d2l-input-radio-label-disabled {
73
+ .d2l-input-radio-label-disabled:not(.d2l-input-radio-label-disabled-tooltip),
74
+ .d2l-input-radio-label-disabled-tooltip > * {
74
75
  opacity: 0.5;
75
76
  }
76
- .d2l-input-radio-label-disabled > .d2l-input-radio,
77
- .d2l-input-radio-label-disabled > input[type="radio"] {
77
+ .d2l-input-radio-label-disabled:not(.d2l-input-radio-label-disabled-tooltip) > .d2l-input-radio,
78
+ .d2l-input-radio-label-disabled:not(.d2l-input-radio-label-disabled-tooltip) > input[type="radio"] {
78
79
  opacity: 1;
79
80
  }
81
+ .d2l-input-radio-label-disabled-tooltip .d2l-input-radio.d2l-hovering,
82
+ .d2l-input-radio-label-disabled-tooltip .d2l-input-radio:hover,
83
+ .d2l-input-radio-label-disabled-tooltip .d2l-input-radio:focus,
84
+ .d2l-input-radio-label-disabled-tooltip .d2l-input-radio-label > input[type="radio"]:hover,
85
+ .d2l-input-radio-label-disabled-tooltip .d2l-input-radio-label > input[type="radio"]:focus {
86
+ background-color: color-mix(in srgb, var(--d2l-color-regolith) 50%, transparent); /* mock background opacity */
87
+ opacity: 1;
88
+ }
89
+
80
90
  .d2l-input-radio-label:last-of-type {
81
91
  margin-bottom: 0;
82
92
  }
@@ -1,3 +1,4 @@
1
+ import '../tooltip/tooltip.js';
1
2
  import { css, html, LitElement, nothing } from 'lit';
2
3
  import { classMap } from 'lit/directives/class-map.js';
3
4
  import { FocusMixin } from '../../mixins/focus/focus-mixin.js';
@@ -32,6 +33,11 @@ class InputRadio extends InputInlineHelpMixin(SkeletonMixin(FocusMixin(PropertyR
32
33
  * @type {boolean}
33
34
  */
34
35
  disabled: { type: Boolean, reflect: true },
36
+ /**
37
+ * Tooltip text displayed when the input is disabled
38
+ * @type {string}
39
+ */
40
+ disabledTooltip: { type: String, attribute: 'disabled-tooltip' },
35
41
  /**
36
42
  * REQUIRED: Label for the input
37
43
  * @type {string}
@@ -48,7 +54,7 @@ class InputRadio extends InputInlineHelpMixin(SkeletonMixin(FocusMixin(PropertyR
48
54
  */
49
55
  value: { type: String },
50
56
  _checked: { state: true },
51
- _firstFocusable: { state: true },
57
+ _focusable: { state: true },
52
58
  _hasSupporting: { state: true },
53
59
  _isHovered: { state: true },
54
60
  _invalid: { state: true }
@@ -87,7 +93,7 @@ class InputRadio extends InputInlineHelpMixin(SkeletonMixin(FocusMixin(PropertyR
87
93
  this.supportingHiddenWhenUnchecked = false;
88
94
  this.value = 'on';
89
95
  this._checked = false;
90
- this._firstFocusable = false;
96
+ this._focusable = false;
91
97
  this._hasSupporting = false;
92
98
  this._isHovered = false;
93
99
  this._isInitFromGroup = false;
@@ -110,20 +116,25 @@ class InputRadio extends InputInlineHelpMixin(SkeletonMixin(FocusMixin(PropertyR
110
116
  }
111
117
  }
112
118
 
119
+ get focusDisabled() {
120
+ return (this.disabled && !this.disabledTooltip) || this.skeleton;
121
+ }
122
+
113
123
  static get focusElementSelector() {
114
124
  return '.d2l-input-radio';
115
125
  }
116
126
 
117
127
  render() {
118
- const disabled = this.disabled || this.skeleton;
128
+ const allowFocus = !this.focusDisabled && this._focusable;
119
129
  const labelClasses = {
120
130
  'd2l-input-radio-label': true,
121
131
  'd2l-input-radio-label-disabled': this.disabled && !this.skeleton,
132
+ 'd2l-input-radio-label-disabled-tooltip': this.disabled && this.disabledTooltip
122
133
  };
123
134
  const radioClasses = {
124
135
  'd2l-input-radio': true,
125
- 'd2l-disabled': this.disabled && !this.skeleton,
126
- 'd2l-hovering': this._isHovered && !disabled,
136
+ 'd2l-disabled': this.focusDisabled && !this.skeleton,
137
+ 'd2l-hovering': this._isHovered && !this.focusDisabled,
127
138
  'd2l-skeletize': true
128
139
  };
129
140
  const supportingClasses = {
@@ -132,28 +143,33 @@ class InputRadio extends InputInlineHelpMixin(SkeletonMixin(FocusMixin(PropertyR
132
143
  };
133
144
  const description = this.description ? html`<div id="${this.#descriptionId}" hidden>${this.description}</div>` : nothing;
134
145
  const ariaDescribedByIds = `${this.description ? this.#descriptionId : ''} ${this._hasInlineHelp ? this.#inlineHelpId : ''}`.trim();
135
- const tabindex = (!disabled && (this._checked || this._firstFocusable)) ? '0' : undefined;
146
+ const disabledTooltip = this.disabled && this.disabledTooltip ?
147
+ html`<d2l-tooltip align="start" class="vdiff-target" for="${this.#inputId}" ?force-show="${this._isHovered}" position="top">${this.disabledTooltip}</d2l-tooltip>` :
148
+ nothing;
136
149
  return html`
137
150
  <div class="${classMap(labelClasses)}" @mouseover="${this.#handleMouseOver}" @mouseout="${this.#handleMouseOut}">
138
151
  <div
139
152
  aria-checked="${this._checked}"
140
153
  aria-describedby="${ifDefined(ariaDescribedByIds.length > 0 ? ariaDescribedByIds : undefined)}"
141
- aria-disabled="${ifDefined(disabled ? 'true' : undefined)}"
154
+ aria-disabled="${ifDefined(this.disabled ? 'true' : undefined)}"
142
155
  aria-invalid="${ifDefined(this._invalid ? 'true' : undefined)}"
143
156
  aria-labelledby="${this.#labelId}"
144
157
  class="${classMap(radioClasses)}"
158
+ id="${this.#inputId}"
145
159
  role="radio"
146
- tabindex="${ifDefined(tabindex)}"></div>
160
+ tabindex="${ifDefined(allowFocus ? '0' : undefined)}"></div>
147
161
  <div id="${this.#labelId}" class="d2l-skeletize">${this.label}</div>
148
162
  </div>
149
163
  ${this._renderInlineHelp(this.#inlineHelpId)}
150
164
  ${description}
165
+ ${disabledTooltip}
151
166
  <div class="${classMap(supportingClasses)}" @change="${this.#handleSupportingChange}"><slot name="supporting" @slotchange="${this.#handleSupportingSlotChange}"></slot></div>
152
167
  `;
153
168
  }
154
169
 
155
170
  #descriptionId = getUniqueId();
156
171
  #inlineHelpId = getUniqueId();
172
+ #inputId = getUniqueId();
157
173
  #labelId = getUniqueId();
158
174
 
159
175
  #handleMouseOut() {
@@ -7175,6 +7175,11 @@
7175
7175
  "description": "ACCESSIBILITY: Additional information communicated to screenreader users when focusing on the input",
7176
7176
  "type": "string"
7177
7177
  },
7178
+ {
7179
+ "name": "disabled-tooltip",
7180
+ "description": "Tooltip text displayed when the input is disabled",
7181
+ "type": "string"
7182
+ },
7178
7183
  {
7179
7184
  "name": "label",
7180
7185
  "description": "REQUIRED: Label for the input",
@@ -7216,6 +7221,12 @@
7216
7221
  "description": "ACCESSIBILITY: Additional information communicated to screenreader users when focusing on the input",
7217
7222
  "type": "string"
7218
7223
  },
7224
+ {
7225
+ "name": "disabledTooltip",
7226
+ "attribute": "disabled-tooltip",
7227
+ "description": "Tooltip text displayed when the input is disabled",
7228
+ "type": "string"
7229
+ },
7219
7230
  {
7220
7231
  "name": "label",
7221
7232
  "attribute": "label",
@@ -7228,6 +7239,10 @@
7228
7239
  "description": "Checked state",
7229
7240
  "type": "boolean"
7230
7241
  },
7242
+ {
7243
+ "name": "focusDisabled",
7244
+ "type": "boolean"
7245
+ },
7231
7246
  {
7232
7247
  "name": "disabled",
7233
7248
  "attribute": "disabled",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.188.0",
3
+ "version": "3.189.0",
4
4
  "description": "A collection of accessible, free, open-source web components for building Brightspace applications",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/BrightspaceUI/core.git",