@brightspace-ui/core 2.93.2 → 2.94.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/README.md +0 -1
- package/components/count-badge/count-badge-icon.js +8 -5
- package/components/count-badge/count-badge.js +9 -6
- package/components/dialog/dialog-mixin.js +2 -2
- package/components/html-block/html-block.js +1 -0
- package/components/inputs/docs/input-search.md +1 -1
- package/components/link/link.js +1 -0
- package/components/menu/menu-item-styles.js +3 -0
- package/components/paging/pager-load-more.js +1 -2
- package/components/scroll-wrapper/scroll-wrapper.js +5 -4
- package/components/switch/switch-mixin.js +8 -7
- package/components/table/table-col-sort-button.js +4 -2
- package/components/tabs/tab-internal.js +4 -2
- package/components/tabs/tabs.js +9 -6
- package/components/tooltip/tooltip-help.js +5 -4
- package/directives/animate/animate.js +2 -2
- package/helpers/README.md +7 -0
- package/helpers/focus.js +6 -0
- package/package.json +2 -3
- package/templates/primary-secondary/primary-secondary.js +18 -9
- package/tools/web-test-runner-helpers.js +12 -0
- package/mixins/focus-visible-polyfill-mixin.js +0 -29
- package/mixins/focus-visible-polyfill-mixin.md +0 -50
package/README.md
CHANGED
|
@@ -68,7 +68,6 @@ npm install @brightspace-ui/core
|
|
|
68
68
|
* [ArrowKeysMixin](mixins/arrow-keys-mixin.md): manage focus with arrow keys
|
|
69
69
|
* [AsyncContainerMixin](mixins/async-container/): manage collective async state
|
|
70
70
|
* [FocusMixin](mixins/focus-mixin.md): delegate focus to a nested element when `focus()` is called
|
|
71
|
-
* [FocusVisiblePolyfillMixin](mixins/focus-visible-polyfill-mixin.md): components can use the `:focus-visible` pseudo-class polyfill
|
|
72
71
|
* [FormElementMixin](components/form/docs/form-element-mixin.md): allow components to participate in forms and validation
|
|
73
72
|
* [InteractiveMixin](mixins/interactive-mixin.md): enables toggling interactive elements inside of nested grids
|
|
74
73
|
* [LabelledMixin](mixins/labelled-mixin.md): label custom elements by referencing elements across DOM scopes
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import '../colors/colors.js';
|
|
2
2
|
import '../icons/icon.js';
|
|
3
|
-
import { css, html, LitElement } from 'lit';
|
|
3
|
+
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
4
4
|
import { CountBadgeMixin } from './count-badge-mixin.js';
|
|
5
|
+
import { getFocusPseudoClass } from '../../helpers/focus.js';
|
|
5
6
|
import { getUniqueId } from '../../helpers/uniqueId.js';
|
|
6
7
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
7
8
|
|
|
@@ -22,9 +23,11 @@ class CountBadgeIcon extends CountBadgeMixin(LitElement) {
|
|
|
22
23
|
|
|
23
24
|
static get styles() {
|
|
24
25
|
return [super.styles, css`
|
|
25
|
-
:host(.focus-visible) d2l-icon,
|
|
26
26
|
:host([focus-ring]) d2l-icon,
|
|
27
|
-
|
|
27
|
+
:host(.focus-visible) d2l-icon,
|
|
28
|
+
d2l-icon.focus-visible,
|
|
29
|
+
:host(:${unsafeCSS(getFocusPseudoClass())}) d2l-icon,
|
|
30
|
+
d2l-icon:${unsafeCSS(getFocusPseudoClass())} {
|
|
28
31
|
box-shadow: 0 0 0 2px var(--d2l-color-celestine);
|
|
29
32
|
outline: none;
|
|
30
33
|
}
|
|
@@ -105,8 +108,8 @@ class CountBadgeIcon extends CountBadgeMixin(LitElement) {
|
|
|
105
108
|
${this.renderCount(numberStyles)}
|
|
106
109
|
<div class="d2l-skeletize d2l-count-badge-wrapper">
|
|
107
110
|
<d2l-icon id="${this._badgeId}"
|
|
108
|
-
icon="${this.icon}"
|
|
109
|
-
tabindex="${ifDefined(tabbable ? '0' : undefined)}"
|
|
111
|
+
icon="${this.icon}"
|
|
112
|
+
tabindex="${ifDefined(tabbable ? '0' : undefined)}"
|
|
110
113
|
aria-labelledby="${ifDefined(this.getAriaLabelId())}"
|
|
111
114
|
role="img">
|
|
112
115
|
</d2l-icon>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import '../colors/colors.js';
|
|
2
|
-
import { css, html, LitElement } from 'lit';
|
|
2
|
+
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
3
3
|
import { CountBadgeMixin } from './count-badge-mixin.js';
|
|
4
|
+
import { getFocusPseudoClass } from '../../helpers/focus.js';
|
|
4
5
|
import { getUniqueId } from '../../helpers/uniqueId.js';
|
|
5
6
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
6
7
|
|
|
@@ -8,9 +9,11 @@ class CountBadge extends CountBadgeMixin(LitElement) {
|
|
|
8
9
|
|
|
9
10
|
static get styles() {
|
|
10
11
|
return [super.styles, css`
|
|
11
|
-
:host(.focus-visible) .d2l-count-badge-wrapper,
|
|
12
12
|
:host([focus-ring]) .d2l-count-badge-wrapper,
|
|
13
|
-
.d2l-count-badge-wrapper
|
|
13
|
+
:host(.focus-visible) .d2l-count-badge-wrapper,
|
|
14
|
+
.d2l-count-badge-wrapper.focus-visible,
|
|
15
|
+
:host(:${unsafeCSS(getFocusPseudoClass())}) .d2l-count-badge-wrapper,
|
|
16
|
+
.d2l-count-badge-wrapper:${unsafeCSS(getFocusPseudoClass())} {
|
|
14
17
|
box-shadow: 0 0 0 2px var(--d2l-color-celestine);
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -22,7 +25,7 @@ class CountBadge extends CountBadgeMixin(LitElement) {
|
|
|
22
25
|
border-radius: 0.65rem;
|
|
23
26
|
outline: none;
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
|
|
26
29
|
:host([size="large"]) .d2l-count-badge-wrapper {
|
|
27
30
|
border-radius: 0.8rem;
|
|
28
31
|
outline: none;
|
|
@@ -38,10 +41,10 @@ class CountBadge extends CountBadgeMixin(LitElement) {
|
|
|
38
41
|
render() {
|
|
39
42
|
const tabbable = (this.tabStop || this.hasTooltip) && !(this.hideZero && this.number === 0) && !this.skeleton;
|
|
40
43
|
const innerHtml = html`
|
|
41
|
-
<div
|
|
44
|
+
<div
|
|
42
45
|
class="d2l-count-badge-wrapper d2l-skeletize"
|
|
43
46
|
id="${this._badgeId}"
|
|
44
|
-
tabindex="${ifDefined(tabbable ? '0' : undefined)}"
|
|
47
|
+
tabindex="${ifDefined(tabbable ? '0' : undefined)}"
|
|
45
48
|
aria-labelledby="${ifDefined(this.getAriaLabelId())}"
|
|
46
49
|
role="img">
|
|
47
50
|
${this.renderCount()}
|
|
@@ -3,7 +3,7 @@ import '../../helpers/viewport-size.js';
|
|
|
3
3
|
import { allowBodyScroll, preventBodyScroll } from '../backdrop/backdrop.js';
|
|
4
4
|
import { clearDismissible, setDismissible } from '../../helpers/dismissible.js';
|
|
5
5
|
import { findComposedAncestor, isComposedAncestor } from '../../helpers/dom.js';
|
|
6
|
-
import { forceFocusVisible, getComposedActiveElement, getNextFocusable, tryApplyFocus } from '../../helpers/focus.js';
|
|
6
|
+
import { forceFocusVisible, getComposedActiveElement, getNextFocusable, isFocusVisibleApplied, tryApplyFocus } from '../../helpers/focus.js';
|
|
7
7
|
import { classMap } from 'lit/directives/class-map.js';
|
|
8
8
|
import { getUniqueId } from '../../helpers/uniqueId.js';
|
|
9
9
|
import { html } from 'lit';
|
|
@@ -411,7 +411,7 @@ export const DialogMixin = superclass => class extends RtlMixin(superclass) {
|
|
|
411
411
|
const activeElement = getComposedActiveElement();
|
|
412
412
|
if (!activeElement
|
|
413
413
|
|| !isComposedAncestor(dialog, activeElement)
|
|
414
|
-
|| !activeElement
|
|
414
|
+
|| !isFocusVisibleApplied(activeElement)) {
|
|
415
415
|
// wait till the dialog is visible for Safari
|
|
416
416
|
requestAnimationFrame(() => this._focusInitial());
|
|
417
417
|
}
|
|
@@ -101,6 +101,7 @@ export const htmlBlockContentStyles = css`
|
|
|
101
101
|
color: var(--d2l-color-celestine-minus-1, #004489);
|
|
102
102
|
text-decoration: underline;
|
|
103
103
|
}
|
|
104
|
+
a.focus-visible,
|
|
104
105
|
a:${unsafeCSS(getFocusPseudoClass())} {
|
|
105
106
|
border-radius: 2px;
|
|
106
107
|
outline: 2px solid var(--d2l-color-celestine, #006fbf);
|
|
@@ -85,4 +85,4 @@ To make your usage of `d2l-input-search` accessible, use the following property
|
|
|
85
85
|
| Attribute | Description |
|
|
86
86
|
|---|---|
|
|
87
87
|
| `description` | Use when label on input does not provide enough context. |
|
|
88
|
-
| label | **REQUIRED** [Acts as a primary label on the input](https://www.w3.org/WAI/tutorials/forms/labels/). Not visible. |
|
|
88
|
+
| `label` | **REQUIRED** [Acts as a primary label on the input](https://www.w3.org/WAI/tutorials/forms/labels/). Not visible. |
|
package/components/link/link.js
CHANGED
|
@@ -20,6 +20,7 @@ export const linkStyles = css`
|
|
|
20
20
|
color: var(--d2l-color-celestine-minus-1);
|
|
21
21
|
text-decoration: underline;
|
|
22
22
|
}
|
|
23
|
+
.d2l-link.focus-visible,
|
|
23
24
|
.d2l-link:${unsafeCSS(getFocusPseudoClass())} {
|
|
24
25
|
border-radius: 2px;
|
|
25
26
|
outline: 2px solid var(--d2l-color-celestine);
|
|
@@ -21,6 +21,8 @@ export const menuItemStyles = css`
|
|
|
21
21
|
color: var(--d2l-menu-foreground-color-hover);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
:host(.focus-visible),
|
|
25
|
+
:host([first].focus-visible),
|
|
24
26
|
:host(:${unsafeCSS(getFocusPseudoClass())}),
|
|
25
27
|
:host([first]:${unsafeCSS(getFocusPseudoClass())}) {
|
|
26
28
|
border-radius: 6px;
|
|
@@ -36,6 +38,7 @@ export const menuItemStyles = css`
|
|
|
36
38
|
opacity: 0.75;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
:host([disabled].focus-visible),
|
|
39
42
|
:host([disabled]:${unsafeCSS(getFocusPseudoClass())}) {
|
|
40
43
|
cursor: default;
|
|
41
44
|
opacity: 0.75;
|
|
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from 'lit';
|
|
|
4
4
|
import { buttonStyles } from '../button/button-styles.js';
|
|
5
5
|
import { findComposedAncestor } from '../../helpers/dom.js';
|
|
6
6
|
import { FocusMixin } from '../../mixins/focus-mixin.js';
|
|
7
|
-
import { FocusVisiblePolyfillMixin } from '../../mixins/focus-visible-polyfill-mixin.js';
|
|
8
7
|
import { formatNumber } from '@brightspace-ui/intl/lib/number.js';
|
|
9
8
|
import { getFirstFocusableDescendant } from '../../helpers/focus.js';
|
|
10
9
|
import { getSeparator } from '@brightspace-ui/intl/lib/list.js';
|
|
@@ -18,7 +17,7 @@ const nativeFocus = document.createElement('div').focus;
|
|
|
18
17
|
* A pager component for load-more paging.
|
|
19
18
|
* @fires d2l-pager-load-more - Dispatched when the user clicks the load-more button. Consumers must call the provided "complete" method once items have been loaded.
|
|
20
19
|
*/
|
|
21
|
-
class LoadMore extends FocusMixin(
|
|
20
|
+
class LoadMore extends FocusMixin(LocalizeCoreElement(LitElement)) {
|
|
22
21
|
|
|
23
22
|
static get properties() {
|
|
24
23
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import '../colors/colors.js';
|
|
2
2
|
import '../icons/icon.js';
|
|
3
|
-
import { css, html, LitElement } from 'lit';
|
|
4
|
-
import {
|
|
3
|
+
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
4
|
+
import { getFocusPseudoClass } from '../../helpers/focus.js';
|
|
5
5
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
6
6
|
import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js';
|
|
7
7
|
import { RtlMixin } from '../../mixins/rtl-mixin.js';
|
|
@@ -14,7 +14,7 @@ const SCROLL_AMOUNT = 0.8;
|
|
|
14
14
|
* Wraps content which may overflow its horizontal boundaries, providing left/right scroll buttons.
|
|
15
15
|
* @slot - User provided content to wrap
|
|
16
16
|
*/
|
|
17
|
-
class ScrollWrapper extends
|
|
17
|
+
class ScrollWrapper extends RtlMixin(LitElement) {
|
|
18
18
|
|
|
19
19
|
static get properties() {
|
|
20
20
|
return {
|
|
@@ -59,7 +59,8 @@ class ScrollWrapper extends FocusVisiblePolyfillMixin(RtlMixin(LitElement)) {
|
|
|
59
59
|
overflow-x: auto;
|
|
60
60
|
overflow-y: var(--d2l-scroll-wrapper-overflow-y, visible);
|
|
61
61
|
}
|
|
62
|
-
.d2l-scroll-wrapper-container.focus-visible
|
|
62
|
+
.d2l-scroll-wrapper-container.focus-visible,
|
|
63
|
+
.d2l-scroll-wrapper-container:${unsafeCSS(getFocusPseudoClass())} {
|
|
63
64
|
box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px var(--d2l-color-celestine), 0 2px 12px 0 rgba(0, 0, 0, 0.15);
|
|
64
65
|
}
|
|
65
66
|
:host([h-scrollbar]) .d2l-scroll-wrapper-container {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import '../colors/colors.js';
|
|
2
|
-
import { css, html } from 'lit';
|
|
2
|
+
import { css, html, unsafeCSS } from 'lit';
|
|
3
3
|
import { classMap } from 'lit/directives/class-map.js';
|
|
4
4
|
import { FocusMixin } from '../../mixins/focus-mixin.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getFocusPseudoClass } from '../../helpers/focus.js';
|
|
6
6
|
import { getUniqueId } from '../../helpers/uniqueId.js';
|
|
7
7
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
8
8
|
import { RtlMixin } from '../../mixins/rtl-mixin.js';
|
|
9
9
|
|
|
10
|
-
export const SwitchMixin = superclass => class extends FocusMixin(RtlMixin(
|
|
10
|
+
export const SwitchMixin = superclass => class extends FocusMixin(RtlMixin(superclass)) {
|
|
11
11
|
|
|
12
12
|
static get properties() {
|
|
13
13
|
return {
|
|
@@ -59,7 +59,8 @@ export const SwitchMixin = superclass => class extends FocusMixin(RtlMixin(Focus
|
|
|
59
59
|
padding: 0.1rem;
|
|
60
60
|
vertical-align: middle;
|
|
61
61
|
}
|
|
62
|
-
.d2l-switch-container.focus-visible
|
|
62
|
+
.d2l-switch-container.focus-visible,
|
|
63
|
+
.d2l-switch-container:${unsafeCSS(getFocusPseudoClass())} {
|
|
63
64
|
border-color: var(--d2l-color-celestine);
|
|
64
65
|
}
|
|
65
66
|
:host([disabled]) .d2l-switch-container {
|
|
@@ -221,9 +222,9 @@ export const SwitchMixin = superclass => class extends FocusMixin(RtlMixin(Focus
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
get _labelContent() {
|
|
224
|
-
return html`<span
|
|
225
|
-
@click='${this._handleClick}'
|
|
226
|
-
@mouseenter='${this._handleSwitchHover}'
|
|
225
|
+
return html`<span
|
|
226
|
+
@click='${this._handleClick}'
|
|
227
|
+
@mouseenter='${this._handleSwitchHover}'
|
|
227
228
|
@mouseleave='${this._handleSwitchHoverLeave}'>
|
|
228
229
|
${this.text}
|
|
229
230
|
</span>`;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import '../colors/colors.js';
|
|
2
2
|
import '../icons/icon.js';
|
|
3
|
-
import { css, html, LitElement } from 'lit';
|
|
3
|
+
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
4
4
|
import { FocusMixin } from '../../mixins/focus-mixin.js';
|
|
5
|
+
import { getFocusPseudoClass } from '../../helpers/focus.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Button for sorting a table column in ascending/descending order.
|
|
@@ -55,7 +56,8 @@ export class TableColSortButton extends FocusMixin(LitElement) {
|
|
|
55
56
|
button:hover {
|
|
56
57
|
text-decoration: underline;
|
|
57
58
|
}
|
|
58
|
-
button:focus-visible
|
|
59
|
+
button:focus-visible,
|
|
60
|
+
button:${unsafeCSS(getFocusPseudoClass())} {
|
|
59
61
|
border-radius: 0.2rem;
|
|
60
62
|
box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px var(--d2l-color-celestine);
|
|
61
63
|
outline-style: none;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../colors/colors.js';
|
|
2
|
-
import { css, html, LitElement } from 'lit';
|
|
2
|
+
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
3
|
+
import { getFocusPseudoClass } from '../../helpers/focus.js';
|
|
3
4
|
import { RtlMixin } from '../../mixins/rtl-mixin.js';
|
|
4
5
|
|
|
5
6
|
const keyCodes = {
|
|
@@ -62,7 +63,8 @@ class Tab extends RtlMixin(LitElement) {
|
|
|
62
63
|
margin-left: 0.6rem;
|
|
63
64
|
margin-right: 0;
|
|
64
65
|
}
|
|
65
|
-
:host(.focus-visible) > .d2l-tab-text
|
|
66
|
+
:host(.focus-visible) > .d2l-tab-text,
|
|
67
|
+
:host(:${unsafeCSS(getFocusPseudoClass())}) > .d2l-tab-text {
|
|
66
68
|
border-radius: 0.3rem;
|
|
67
69
|
box-shadow: 0 0 0 2px var(--d2l-color-celestine);
|
|
68
70
|
color: var(--d2l-color-celestine);
|
package/components/tabs/tabs.js
CHANGED
|
@@ -2,12 +2,12 @@ import '../colors/colors.js';
|
|
|
2
2
|
import '../icons/icon.js';
|
|
3
3
|
import '../../helpers/queueMicrotask.js';
|
|
4
4
|
import './tab-internal.js';
|
|
5
|
-
import { css, html, LitElement } from 'lit';
|
|
5
|
+
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
6
6
|
import { cssEscape, findComposedAncestor } from '../../helpers/dom.js';
|
|
7
7
|
import { ArrowKeysMixin } from '../../mixins/arrow-keys-mixin.js';
|
|
8
8
|
import { bodyCompactStyles } from '../typography/styles.js';
|
|
9
9
|
import { classMap } from 'lit/directives/class-map.js';
|
|
10
|
-
import {
|
|
10
|
+
import { getFocusPseudoClass } from '../../helpers/focus.js';
|
|
11
11
|
import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
|
|
12
12
|
import { repeat } from 'lit/directives/repeat.js';
|
|
13
13
|
import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js';
|
|
@@ -52,7 +52,7 @@ if (!Array.prototype.findIndex) {
|
|
|
52
52
|
* @slot ext - Additional content (e.g., a button) positioned at right
|
|
53
53
|
* @fires d2l-tabs-initialized - Dispatched when the component is initialized
|
|
54
54
|
*/
|
|
55
|
-
class Tabs extends LocalizeCoreElement(ArrowKeysMixin(RtlMixin(
|
|
55
|
+
class Tabs extends LocalizeCoreElement(ArrowKeysMixin(RtlMixin(LitElement))) {
|
|
56
56
|
|
|
57
57
|
static get properties() {
|
|
58
58
|
return {
|
|
@@ -182,14 +182,17 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(RtlMixin(FocusVisiblePolyf
|
|
|
182
182
|
border: 0;
|
|
183
183
|
}
|
|
184
184
|
.d2l-tabs-scroll-button[disabled]:hover,
|
|
185
|
-
.d2l-tabs-scroll-button[disabled].focus-visible
|
|
185
|
+
.d2l-tabs-scroll-button[disabled].focus-visible,
|
|
186
|
+
.d2l-tabs-scroll-button[disabled]:${unsafeCSS(getFocusPseudoClass())} {
|
|
186
187
|
background-color: transparent;
|
|
187
188
|
}
|
|
188
189
|
.d2l-tabs-scroll-button:hover,
|
|
189
|
-
.d2l-tabs-scroll-button.focus-visible
|
|
190
|
+
.d2l-tabs-scroll-button.focus-visible,
|
|
191
|
+
.d2l-tabs-scroll-button:${unsafeCSS(getFocusPseudoClass())} {
|
|
190
192
|
background-color: var(--d2l-color-gypsum);
|
|
191
193
|
}
|
|
192
|
-
.d2l-tabs-scroll-button.focus-visible
|
|
194
|
+
.d2l-tabs-scroll-button.focus-visible,
|
|
195
|
+
.d2l-tabs-scroll-button:${unsafeCSS(getFocusPseudoClass())} {
|
|
193
196
|
box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px var(--d2l-color-celestine);
|
|
194
197
|
}
|
|
195
198
|
.d2l-panels-container-no-whitespace ::slotted(*) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import '../colors/colors.js';
|
|
2
2
|
import '../tooltip/tooltip.js';
|
|
3
|
-
import { css, html, LitElement, nothing } from 'lit';
|
|
3
|
+
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
|
|
4
4
|
import { bodySmallStyles } from '../typography/styles.js';
|
|
5
5
|
import { classMap } from 'lit/directives/class-map.js';
|
|
6
6
|
import { FocusMixin } from '../../mixins/focus-mixin.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getFocusPseudoClass } from '../../helpers/focus.js';
|
|
8
8
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
9
9
|
import { SkeletonMixin } from '../skeleton/skeleton-mixin.js';
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ import { SkeletonMixin } from '../skeleton/skeleton-mixin.js';
|
|
|
12
12
|
* A component used to display additional information when users focus or hover over some text.
|
|
13
13
|
* @slot - Default content placed inside of the tooltip
|
|
14
14
|
*/
|
|
15
|
-
class TooltipHelp extends SkeletonMixin(FocusMixin(
|
|
15
|
+
class TooltipHelp extends SkeletonMixin(FocusMixin(LitElement)) {
|
|
16
16
|
|
|
17
17
|
static get properties() {
|
|
18
18
|
return {
|
|
@@ -60,7 +60,8 @@ class TooltipHelp extends SkeletonMixin(FocusMixin(FocusVisiblePolyfillMixin(Lit
|
|
|
60
60
|
#d2l-tooltip-help-text:focus {
|
|
61
61
|
outline-style: none;
|
|
62
62
|
}
|
|
63
|
-
#d2l-tooltip-help-text.focus-visible
|
|
63
|
+
#d2l-tooltip-help-text.focus-visible,
|
|
64
|
+
#d2l-tooltip-help-text:${unsafeCSS(getFocusPseudoClass())} {
|
|
64
65
|
border-radius: 0.05rem;
|
|
65
66
|
outline: 2px solid var(--d2l-color-celestine);
|
|
66
67
|
outline-offset: 0.05rem;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { directive, PartType } from 'lit/directive.js';
|
|
2
|
-
import { getComposedActiveElement, getNextFocusable } from '../../helpers/focus.js';
|
|
2
|
+
import { getComposedActiveElement, getNextFocusable, isFocusVisibleApplied } from '../../helpers/focus.js';
|
|
3
3
|
import { AsyncDirective } from 'lit/async-directive.js';
|
|
4
4
|
import { isComposedAncestor } from '../../helpers/dom.js';
|
|
5
5
|
import { noChange } from 'lit';
|
|
@@ -180,7 +180,7 @@ class AnimationState {
|
|
|
180
180
|
|
|
181
181
|
// if focus is inside and was placed by keyboard, move it to next focusable
|
|
182
182
|
const activeElem = getComposedActiveElement();
|
|
183
|
-
if (activeElem && activeElem
|
|
183
|
+
if (activeElem && isFocusVisibleApplied(activeElem)) {
|
|
184
184
|
const focusIsInside = isComposedAncestor(this.elem, activeElem);
|
|
185
185
|
if (focusIsInside) {
|
|
186
186
|
const nextFocusable = getNextFocusable(activeElem);
|
package/helpers/README.md
CHANGED
|
@@ -130,6 +130,10 @@ getComposedActiveElement()
|
|
|
130
130
|
getFirstFocusableDescendant(node, includeHidden, predicate, includeTabbablesOnly)
|
|
131
131
|
|
|
132
132
|
// gets the focus pseudo-class to used in selectors (focus-visible if supported, or focus)
|
|
133
|
+
// Always also include `.focus-visible` as a selector, to support `forceFocusVisible`. Usage:
|
|
134
|
+
// css`
|
|
135
|
+
// some-element.focus-visible, some-element:${unsafeCSS(getFocusPseudoClass())} { ... }
|
|
136
|
+
// `
|
|
133
137
|
getFocusPseudoClass()
|
|
134
138
|
|
|
135
139
|
// gets the last focusable descendant given a node, including those within the shadow DOM
|
|
@@ -147,6 +151,9 @@ getPreviousFocusableAncestor(node, includeHidden, includeTabbablesOnly)
|
|
|
147
151
|
// returns true/false whether the element is focusable
|
|
148
152
|
isFocusable(node, includeHidden, includeTabbablesOnly, includeDisabled)
|
|
149
153
|
|
|
154
|
+
// returns true/false whether the element has :focus-visible or .focus-visible applied
|
|
155
|
+
isFocusVisibleApplied(node)
|
|
156
|
+
|
|
150
157
|
// returns true/false whether the :focus-visible is supported
|
|
151
158
|
isFocusVisibleSupported()
|
|
152
159
|
|
package/helpers/focus.js
CHANGED
|
@@ -186,6 +186,12 @@ export function isFocusable(node, includeHidden, includeTabbablesOnly, includeDi
|
|
|
186
186
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
export function isFocusVisibleApplied(node) {
|
|
190
|
+
if (!node) return false;
|
|
191
|
+
if (node.classList.contains('focus-visible')) return true;
|
|
192
|
+
return isFocusVisibleSupported() && node.parentNode?.querySelector(':focus-visible') === node;
|
|
193
|
+
}
|
|
194
|
+
|
|
189
195
|
let _isFocusVisibleSupported;
|
|
190
196
|
|
|
191
197
|
export function isFocusVisibleSupported() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brightspace-ui/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.94.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",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test:axe": "web-test-runner --group aXe",
|
|
24
24
|
"test:headless": "web-test-runner --group default",
|
|
25
25
|
"test:headless:watch": "web-test-runner --group default --watch",
|
|
26
|
-
"test:translations": "mfv -e -s en -p ./lang/"
|
|
26
|
+
"test:translations": "mfv -e -s en -p ./lang/ -i untranslated"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"custom-elements.json",
|
|
@@ -71,7 +71,6 @@
|
|
|
71
71
|
"@brightspace-ui/intl": "^3",
|
|
72
72
|
"@formatjs/intl-pluralrules": "^1",
|
|
73
73
|
"@open-wc/dedupe-mixin": "^1",
|
|
74
|
-
"focus-visible": "^5",
|
|
75
74
|
"ifrau": "^0.40",
|
|
76
75
|
"intl-messageformat": "^7",
|
|
77
76
|
"lit": "^2",
|
|
@@ -2,9 +2,9 @@ import '../../components/colors/colors.js';
|
|
|
2
2
|
import '../../components/icons/icon-custom.js';
|
|
3
3
|
import '../../components/icons/icon.js';
|
|
4
4
|
import '../../components/offscreen/offscreen.js';
|
|
5
|
-
import { css, html, LitElement } from 'lit';
|
|
5
|
+
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
6
6
|
import { classMap } from 'lit/directives/class-map.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getFocusPseudoClass } from '../../helpers/focus.js';
|
|
8
8
|
import { getUniqueId } from '../../helpers/uniqueId.js';
|
|
9
9
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
10
10
|
import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
|
|
@@ -536,7 +536,7 @@ class MobileTouchResizer extends Resizer {
|
|
|
536
536
|
* @fires d2l-template-primary-secondary-resize-start - Dispatched when a user begins moving the divider.
|
|
537
537
|
* @fires d2l-template-primary-secondary-resize-end - Dispatched when a user finishes moving the divider.
|
|
538
538
|
*/
|
|
539
|
-
class TemplatePrimarySecondary extends
|
|
539
|
+
class TemplatePrimarySecondary extends RtlMixin(LocalizeCoreElement(LitElement)) {
|
|
540
540
|
|
|
541
541
|
static get properties() {
|
|
542
542
|
return {
|
|
@@ -743,7 +743,9 @@ class TemplatePrimarySecondary extends FocusVisiblePolyfillMixin(RtlMixin(Locali
|
|
|
743
743
|
width: 0.1rem;
|
|
744
744
|
}
|
|
745
745
|
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle-right,
|
|
746
|
-
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle-left
|
|
746
|
+
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle-left,
|
|
747
|
+
.d2l-template-primary-secondary-divider:${unsafeCSS(getFocusPseudoClass())} .d2l-template-primary-secondary-divider-handle-right,
|
|
748
|
+
.d2l-template-primary-secondary-divider:${unsafeCSS(getFocusPseudoClass())} .d2l-template-primary-secondary-divider-handle-left {
|
|
747
749
|
display: block;
|
|
748
750
|
}
|
|
749
751
|
:host(:not([dir="rtl"]):not([secondary-first])) [data-is-expanded] .d2l-template-primary-secondary-divider-handle-left,
|
|
@@ -776,7 +778,9 @@ class TemplatePrimarySecondary extends FocusVisiblePolyfillMixin(RtlMixin(Locali
|
|
|
776
778
|
box-shadow: none;
|
|
777
779
|
}
|
|
778
780
|
:host([resizable]) .d2l-template-primary-secondary-divider.focus-visible,
|
|
779
|
-
:host([resizable][dir="rtl"]) .d2l-template-primary-secondary-divider.focus-visible
|
|
781
|
+
:host([resizable][dir="rtl"]) .d2l-template-primary-secondary-divider.focus-visible,
|
|
782
|
+
:host([resizable]) .d2l-template-primary-secondary-divider:${unsafeCSS(getFocusPseudoClass())},
|
|
783
|
+
:host([resizable][dir="rtl"]) .d2l-template-primary-secondary-divider:${unsafeCSS(getFocusPseudoClass())} {
|
|
780
784
|
background-color: var(--d2l-color-celestine);
|
|
781
785
|
}
|
|
782
786
|
.d2l-template-primary-secondary-divider:focus .d2l-template-primary-secondary-divider-handle-line::before,
|
|
@@ -786,7 +790,9 @@ class TemplatePrimarySecondary extends FocusVisiblePolyfillMixin(RtlMixin(Locali
|
|
|
786
790
|
background-color: var(--d2l-color-ferrite);
|
|
787
791
|
}
|
|
788
792
|
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle-line::before,
|
|
789
|
-
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle-line::after
|
|
793
|
+
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle-line::after,
|
|
794
|
+
.d2l-template-primary-secondary-divider:${unsafeCSS(getFocusPseudoClass())} .d2l-template-primary-secondary-divider-handle-line::before,
|
|
795
|
+
.d2l-template-primary-secondary-divider:${unsafeCSS(getFocusPseudoClass())} .d2l-template-primary-secondary-divider-handle-line::after {
|
|
790
796
|
background-color: white;
|
|
791
797
|
}
|
|
792
798
|
|
|
@@ -913,13 +919,15 @@ class TemplatePrimarySecondary extends FocusVisiblePolyfillMixin(RtlMixin(Locali
|
|
|
913
919
|
height: 1rem;
|
|
914
920
|
width: 2.2rem;
|
|
915
921
|
}
|
|
916
|
-
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle
|
|
922
|
+
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle,
|
|
923
|
+
.d2l-template-primary-secondary-divider:${unsafeCSS(getFocusPseudoClass())} .d2l-template-primary-secondary-divider-handle {
|
|
917
924
|
box-shadow: none;
|
|
918
925
|
height: 1.2rem;
|
|
919
926
|
right: 17px;
|
|
920
927
|
width: 2.6rem;
|
|
921
928
|
}
|
|
922
|
-
:host([dir="rtl"]) .d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle
|
|
929
|
+
:host([dir="rtl"]) .d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle,
|
|
930
|
+
:host([dir="rtl"]) .d2l-template-primary-secondary-divider:${unsafeCSS(getFocusPseudoClass())} .d2l-template-primary-secondary-divider-handle {
|
|
923
931
|
left: 17px;
|
|
924
932
|
right: auto;
|
|
925
933
|
}
|
|
@@ -927,7 +935,8 @@ class TemplatePrimarySecondary extends FocusVisiblePolyfillMixin(RtlMixin(Locali
|
|
|
927
935
|
color: white;
|
|
928
936
|
display: block;
|
|
929
937
|
}
|
|
930
|
-
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle-mobile
|
|
938
|
+
.d2l-template-primary-secondary-divider.focus-visible .d2l-template-primary-secondary-divider-handle-mobile,
|
|
939
|
+
.d2l-template-primary-secondary-divider:${unsafeCSS(getFocusPseudoClass())} .d2l-template-primary-secondary-divider-handle-mobile {
|
|
931
940
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.2rem var(--d2l-color-celestine);
|
|
932
941
|
right: 0.2rem;
|
|
933
942
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
|
|
2
|
+
|
|
3
|
+
export const focusWithKeyboard = async(element) => {
|
|
4
|
+
await sendKeys({ press: 'Tab' });
|
|
5
|
+
element.focus({ focusVisible: true });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const focusWithMouse = async(element) => {
|
|
9
|
+
const { x, y } = element.getBoundingClientRect();
|
|
10
|
+
await sendMouse({ type: 'click', position: [Math.ceil(x), Math.ceil(y)] });
|
|
11
|
+
await resetMouse();
|
|
12
|
+
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
let polyfillLoading = false;
|
|
2
|
-
|
|
3
|
-
export const FocusVisiblePolyfillMixin = superclass => class extends superclass {
|
|
4
|
-
|
|
5
|
-
connectedCallback() {
|
|
6
|
-
|
|
7
|
-
super.connectedCallback();
|
|
8
|
-
|
|
9
|
-
if (!this.shadowRoot) return;
|
|
10
|
-
|
|
11
|
-
if (window.applyFocusVisiblePolyfill) {
|
|
12
|
-
window.applyFocusVisiblePolyfill(this.shadowRoot);
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
window.addEventListener('focus-visible-polyfill-ready', () => {
|
|
17
|
-
// somehow it's occasionally still not available
|
|
18
|
-
if (this.shadowRoot && window.applyFocusVisiblePolyfill) {
|
|
19
|
-
window.applyFocusVisiblePolyfill(this.shadowRoot);
|
|
20
|
-
}
|
|
21
|
-
}, { once: true });
|
|
22
|
-
|
|
23
|
-
if (!polyfillLoading) {
|
|
24
|
-
polyfillLoading = true;
|
|
25
|
-
import('focus-visible');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# FocusVisiblePolyfillMixin
|
|
2
|
-
|
|
3
|
-
From [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible):
|
|
4
|
-
|
|
5
|
-
> The `:focus-visible` pseudo-class applies while an element matches the `:focus` pseudo-class and the user agent determines via heuristics that the focus should be made evident on the element.
|
|
6
|
-
|
|
7
|
-
In order words, it allows you to provide focus styles for an element that only get applied when the user accessed it via keyboard. But today only Chrome supports `:focus-visible`, so a polyfill is required.
|
|
8
|
-
|
|
9
|
-
The `FocusVisiblePolyfillMixin` adds support to a component to use the [focus-visible polyfill](https://github.com/WICG/focus-visible), ensuring that the polyfill is lazy loaded only once and that it's properly applied to the component's shadow root.
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
Apply the mixin and target the `focus-visible` CSS class for styles you'd like to apply when focus should be visible.
|
|
14
|
-
|
|
15
|
-
```js
|
|
16
|
-
import { FocusVisiblePolyfillMixin } from '@brightspace-ui/core/mixins/focus-visible-polyfill-mixin.js';
|
|
17
|
-
|
|
18
|
-
class MyComponent extends FocusVisiblePolyfillMixin(LitElement) {
|
|
19
|
-
static get styles() {
|
|
20
|
-
return css`
|
|
21
|
-
/* styles to apply when clicked or focused with a keyboard */
|
|
22
|
-
button:hover,
|
|
23
|
-
button:focus {
|
|
24
|
-
background-color: #cccccc;
|
|
25
|
-
}
|
|
26
|
-
/* styles to apply only when focused with a keyboard */
|
|
27
|
-
button.focus-visible {
|
|
28
|
-
outline: 2px solid black;
|
|
29
|
-
}
|
|
30
|
-
`;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
**Learn More:** [focus-visible polyfill documentation](https://github.com/WICG/focus-visible)
|
|
36
|
-
|
|
37
|
-
## Forcing Visible Focus
|
|
38
|
-
|
|
39
|
-
When applying focus programatically in JavaScript via `elem.focus()`, unless a `Tab` keyboard event has just occurred, a visible focus ring will not be shown. In certain circumstances, forcing focus to be visible is desired -- like when moving focus from one element to another.
|
|
40
|
-
|
|
41
|
-
For example, opening a dialog moves the user's focus from an opener to inside the dialog and then closing the dialog moves it back to the opener. In cases like this, not being able to clearly see which element has focus can be disorienting for the user.
|
|
42
|
-
|
|
43
|
-
To force visible focus, use the `forceFocusVisible()` helper:
|
|
44
|
-
|
|
45
|
-
```js
|
|
46
|
-
import { forceFocusVisible } from '@brightspace-ui/core/helpers/focus.js';
|
|
47
|
-
|
|
48
|
-
// focus and force a visible focus ring
|
|
49
|
-
forceFocusVisible(elem);
|
|
50
|
-
```
|