@brightspace-ui/core 1.209.1 → 1.212.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.
@@ -16,6 +16,11 @@ class List extends SelectionMixin(LitElement) {
16
16
 
17
17
  static get properties() {
18
18
  return {
19
+ /**
20
+ * Whether to the user can drag multiple items
21
+ * @type {boolean}
22
+ */
23
+ dragMultiple: { type: Boolean, attribute: 'drag-multiple' },
19
24
  /**
20
25
  * Whether to extend the separators beyond the content's edge
21
26
  * @type {boolean}
@@ -48,6 +53,7 @@ class List extends SelectionMixin(LitElement) {
48
53
 
49
54
  constructor() {
50
55
  super();
56
+ this.dragMultiple = false;
51
57
  this.extendSeparators = false;
52
58
  this.grid = false;
53
59
  this._listItemChanges = [];
@@ -91,17 +97,37 @@ class List extends SelectionMixin(LitElement) {
91
97
  `;
92
98
  }
93
99
 
100
+ getItems() {
101
+ const slot = this.shadowRoot.querySelector('slot:not([name])');
102
+ if (!slot) return [];
103
+ return slot.assignedNodes({ flatten: true }).filter((node) => {
104
+ return node.nodeType === Node.ELEMENT_NODE && (node.role === 'rowgroup' || node.role === 'listitem');
105
+ });
106
+ }
107
+
108
+ getListItemByKey(key) {
109
+ const items = this.getItems();
110
+ for (let i = 0; i < items.length; i++) {
111
+ if (items[i].key === key) return items[i];
112
+ if (items[i]._selectionProvider) {
113
+ const tempItem = items[i]._selectionProvider.getListItemByKey(key);
114
+ if (tempItem) return tempItem;
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
94
120
  getListItemCount() {
95
- return this._getItems().length;
121
+ return this.getItems().length;
96
122
  }
97
123
 
98
124
  getListItemIndex(item) {
99
- return this._getItems().indexOf(item);
125
+ return this.getItems().indexOf(item);
100
126
  }
101
127
 
102
128
  getSelectedListItems(includeNested) {
103
129
  let selectedItems = [];
104
- this._getItems().forEach(item => {
130
+ this.getItems().forEach(item => {
105
131
  if (item.selected) selectedItems.push(item);
106
132
  if (includeNested && item._selectionProvider) {
107
133
  selectedItems = [...selectedItems, ...item._selectionProvider.getSelectedListItems(includeNested)];
@@ -116,7 +142,7 @@ class List extends SelectionMixin(LitElement) {
116
142
 
117
143
  let keys = selectionInfo.keys;
118
144
 
119
- this._getItems().forEach(item => {
145
+ this.getItems().forEach(item => {
120
146
  if (item._selectionProvider) {
121
147
  keys = [...keys, ...item._selectionProvider.getSelectionInfo(true).keys];
122
148
  }
@@ -125,14 +151,6 @@ class List extends SelectionMixin(LitElement) {
125
151
  return new SelectionInfo(keys, selectionInfo.state);
126
152
  }
127
153
 
128
- _getItems() {
129
- const slot = this.shadowRoot.querySelector('slot:not([name])');
130
- if (!slot) return [];
131
- return slot.assignedNodes({ flatten: true }).filter((node) => {
132
- return node.nodeType === Node.ELEMENT_NODE && (node.role === 'listitem' || node.tagName.includes('LIST-ITEM'));
133
- });
134
- }
135
-
136
154
  _handleKeyDown(e) {
137
155
  if (!this.grid || this.slot === 'nested' || e.keyCode !== keyCodes.TAB) return;
138
156
  e.preventDefault();
@@ -1,4 +1,6 @@
1
- export const MenuItemMixin = superclass => class extends superclass {
1
+
2
+ import { FocusVisiblePolyfillMixin } from '../../mixins/focus-visible-polyfill-mixin.js';
3
+ export const MenuItemMixin = superclass => class extends FocusVisiblePolyfillMixin(superclass) {
2
4
 
3
5
  static get properties() {
4
6
  return {
@@ -15,9 +15,10 @@ export const menuItemStyles = css`
15
15
  outline: none;
16
16
  width: 100%;
17
17
  }
18
- :host(:focus),
18
+
19
19
  :host(:hover),
20
- :host([first]:focus),
20
+ :host(.focus-visible),
21
+ :host(.focus-visible[first]),
21
22
  :host([first]:hover) {
22
23
  background-color: var(--d2l-menu-background-color-hover);
23
24
  border-bottom: 1px solid var(--d2l-menu-border-color-hover);
@@ -26,7 +27,7 @@ export const menuItemStyles = css`
26
27
  z-index: 2;
27
28
  }
28
29
 
29
- :host([disabled]), :host([disabled]:hover), :host([disabled]:focus) {
30
+ :host([disabled]), :host([disabled]:hover), :host([disabled].focus-visible) {
30
31
  cursor: default;
31
32
  opacity: 0.75;
32
33
  }
@@ -39,7 +40,7 @@ export const menuItemStyles = css`
39
40
  border-top-color: transparent;
40
41
  }
41
42
 
42
- :host([last]:focus),
43
+ :host([last].focus-visible),
43
44
  :host([last]:hover) {
44
45
  border-bottom-color: var(--d2l-menu-border-color-hover);
45
46
  }
@@ -2,6 +2,7 @@ import '../colors/colors.js';
2
2
  import '../icons/icon.js';
3
3
  import './menu-item-return.js';
4
4
  import { css, html, LitElement } from 'lit-element/lit-element.js';
5
+ import { FocusVisiblePolyfillMixin } from '../../mixins/focus-visible-polyfill-mixin.js';
5
6
  import { HierarchicalViewMixin } from '../hierarchical-view/hierarchical-view-mixin.js';
6
7
  import { ThemeMixin } from '../../mixins/theme-mixin.js';
7
8
 
@@ -20,7 +21,7 @@ const keyCodes = {
20
21
  * @slot - Menu items
21
22
  * @fires d2l-menu-resize - Dispatched when size of menu changes (e.g., when nested menu of a different size is opened)
22
23
  */
23
- class Menu extends ThemeMixin(HierarchicalViewMixin(LitElement)) {
24
+ class Menu extends ThemeMixin(HierarchicalViewMixin(FocusVisiblePolyfillMixin(LitElement))) {
24
25
 
25
26
  static get properties() {
26
27
  return {
@@ -0,0 +1,177 @@
1
+ # Subscriber Controllers
2
+
3
+ The `SubscriberRegistryController` and the corresponding `*SubscriberController`s can be used to create a subscription system within your app. Components can setup a subscriber registry instance to keep track of all components subscribed to them with the `SubscriberRegistryController`. Whenever it makes sense to do so, they can iterate over their subscribers to perform some action, update them with new data, etc. Components can subscribe themselves to different registries using the `IdSubscriberController` or the `EventSubscriberController`. This system supports a many-to-many relationship - registry components can contain multiple registry instances with multiple subscribers in each, and subscriber components can subscribe to multiple different registries.
4
+
5
+ ## Usage
6
+
7
+ Create an instance of the `SubscriberRegistryController` in the component that will be responsible for providing some data or performing some function on all its subscribers:
8
+
9
+ ```js
10
+ import { SubscriberRegistryController } from '@brightspace-ui/core/controllers/subscriber/subscriberControllers.js';
11
+
12
+ class CableSubscription extends LitElement {
13
+ constructor() {
14
+ super();
15
+ this._sportsSubscribers = new SubscriberRegistryController(this,
16
+ { onSubscribe: this._unlockSportsChannels.bind(this) },
17
+ { eventName: 'd2l-channels-subscribe-sports' }
18
+ );
19
+
20
+ this._movieSubscribers = new SubscriberRegistryController(this, {},
21
+ { onSubscribe: this._unlockMovieChannels.bind(this), updateSubscribers: this._sendMovieGuide.bind(this) },
22
+ { eventName: 'd2l-channels-subscribe-movies' }
23
+ );
24
+
25
+ // This controller only supports registering by id - no event is needed
26
+ this._kidsChannelSubscribers = new SubscriberRegistryController(this,
27
+ { onSubscribe: this._unlockKidsChannels.bind(this) }, {});
28
+ }
29
+
30
+ getController(controllerId) {
31
+ if (controllerId === 'sports') {
32
+ return this._sportsSubscribers;
33
+ } else if (controllerId === 'movies') {
34
+ return this._movieSubscribers;
35
+ } else if (controllerId === 'kids') {
36
+ return this._kidsChannelSubscribers;
37
+ }
38
+ }
39
+
40
+ _sendMovieGuide(subscribers) {
41
+ subscribers.forEach(subscriber => subscriber.updateGuide(new MovieGuide(new Date().getMonth())));
42
+ }
43
+
44
+ _unlockMovieChannels(subscriber) {
45
+ subscriber.addChannels([330, 331, 332, 333, 334, 335]);
46
+ }
47
+
48
+ ...
49
+ }
50
+ ```
51
+
52
+ When creating the controller, you can pass in callbacks to run whenever a subscriber is added, removed, or `updateSubscribers` is called (which handles request debouncing for you).
53
+
54
+ The `*subscriberController`s will use a `getController` method that needs to be exposed on the registry component. If you only have one `SubscriberRegistryController` you can simple return that. If you have multiple, you will return the proper controller depending on the id the subscriber component passed to you.
55
+
56
+ Once this has been set up, components can subscribe to particular registries two different ways:
57
+ 1. Using a matching event name with `EventSubscriberController`. The component will need to be a child of the registry component for this to work.
58
+ 2. By pointing to the registry component's id with `IdSubscriberController`. The component will need to be in the same DOM scope as the registry component for this to work.
59
+
60
+ Like the `SubscriberRegistryController`, these `*subscriberController`s take optional callbacks to throw at different points in the subscription process.
61
+
62
+ ```js
63
+ import { EventSubscriberController, IdSubscriberController } from '@brightspace-ui/core/controllers/subscriber/subscriberControllers.js';
64
+
65
+ class GeneralViewer extends LitElement {
66
+ static get properties() {
67
+ return {
68
+ _subscribedChannels: { type: Object }
69
+ };
70
+ }
71
+
72
+ constructor() {
73
+ super();
74
+ this._subscribedChannels = new Set();
75
+
76
+ this._sportsSubscription = new EventSubscriberController(this,
77
+ { onError: this._onSportsError.bind(this) }
78
+ { eventName: 'd2l-channels-subscribe-sports', controllerId: 'sports' }
79
+ );
80
+
81
+ this._movieSubscription = new EventSubscriberController(this, {},
82
+ { eventName: 'd2l-channels-subscribe-movies', controllerId: 'movies' }
83
+ );
84
+ }
85
+
86
+ addChannels(channels) {
87
+ channels.forEach(channel => this._subscribedChannels.add(channel));
88
+ }
89
+
90
+ _onSportsError() {
91
+ throw new Error('Where are the sports?');
92
+ }
93
+
94
+ ...
95
+ }
96
+
97
+ class YoungerViewer extends LitElement {
98
+ static get properties() {
99
+ return {
100
+ for: { type: String },
101
+ _subscribedChannels: { type: Object }
102
+ };
103
+ }
104
+
105
+ constructor() {
106
+ super();
107
+ this._subscribedChannels = new Set();
108
+
109
+ this._kidsSubscription = new IdSubscriberController(this,
110
+ { onSubscribe: this._onSubscribe.bind(this), onUnsubscribe: this._onUnsubscribe.bind(this) },
111
+ { idPropertyName: 'for', controllerId: 'kids' }
112
+ );
113
+ }
114
+
115
+ addChannels(channels) {
116
+ channels.forEach(channel => this._subscribedChannels.add(channel));
117
+ }
118
+
119
+ _onSubscribe(cableProvider) {
120
+ console.log(`Subscribed with ${cableProvider.id} successfully.`);
121
+ }
122
+
123
+ _onUnsubscribe(cableProviderId) {
124
+ console.log(`Looks like ${cableProviderId} is having an outage again.`);
125
+ }
126
+
127
+ ...
128
+ }
129
+ ```
130
+
131
+ An example of what this could look like altogether:
132
+ ```html
133
+ <cable-subscription id="rogers">
134
+ <general-viewer></general-viewer>
135
+ </cable-subscription>
136
+ <younger-viewer for="rogers"></younger-viewer>
137
+ ```
138
+
139
+ NOTE: Until we are on Lit 2, the controller lifecycle events will need to be manually called:
140
+ ```js
141
+ connectedCallback() {
142
+ super.connectedCallback();
143
+ if (this._subscriptionController) this._subscriptionController.hostConnected();
144
+ }
145
+
146
+ disconnectedCallback() {
147
+ super.disconnectedCallback();
148
+ if (this._subscriptionController) this._subscriptionController.hostDisconnected();
149
+ }
150
+
151
+ updated(changedProperties) {
152
+ super.updated(changedProperties);
153
+ if (this._subscriptionController) this._subscriptionController.hostUpdated(changedProperties);
154
+ }
155
+ ```
156
+
157
+ ## Available Callbacks
158
+
159
+ ### SubscriberRegistryController
160
+ | Callback Name | Description | Passed to Callback |
161
+ |---|---|---|
162
+ | `onSubscribe` | Runs whenever a new subscriber is added | Subscriber that was just subscribed |
163
+ | `onUnsubscribe` | Runs whenever a subscriber is removed | Subscriber that was just unsubscribed |
164
+ | `updateSubscribers` | Runs whenever `updateSubscribers` is called on the controller, handles debouncing requests for you | Map of all current subscribers |
165
+
166
+ ### EventSubscriberController
167
+ | Callback Name | Description | Passed to Callback |
168
+ |---|---|---|
169
+ | `onSubscribe` | Runs when successfully subscribed to a registry component | Registry that was just subscribed to |
170
+ | `onError` | Runs if the event was unacknowledged and no registry component was found | None |
171
+
172
+ ### IdSubscriberController
173
+ | Callback Name | Description | Passed to Callback |
174
+ |---|---|---|
175
+ | `onSubscribe` | Runs whenever a registry component is successfully subscribed to | Registry that was just subscribed to |
176
+ | `onUnsubscribe` | Runs whenever we unsubscribe to a registry (because it is now gone, or its id was removed from the id property list) | Id of the registry that was just unsubscribed to |
177
+ | `onError` | Runs if no registry component was found for an id | Id of the registry we do not have a component for |
@@ -0,0 +1,180 @@
1
+ import { cssEscape } from '../../helpers/dom.js';
2
+
3
+ export class SubscriberRegistryController {
4
+
5
+ constructor(host, callbacks, options) {
6
+ this._host = host;
7
+ this._callbacks = callbacks || {};
8
+ this._eventName = options && options.eventName;
9
+ this._subscribers = new Map();
10
+
11
+ this._handleSubscribe = this._handleSubscribe.bind(this);
12
+ }
13
+
14
+ get subscribers() {
15
+ return this._subscribers;
16
+ }
17
+
18
+ hostConnected() {
19
+ if (this._eventName) this._host.addEventListener(this._eventName, this._handleSubscribe);
20
+ }
21
+
22
+ hostDisconnected() {
23
+ if (this._eventName) this._host.removeEventListener(this._eventName, this._handleSubscribe);
24
+ }
25
+
26
+ subscribe(target) {
27
+ if (this._subscribers.has(target)) return;
28
+ this._subscribers.set(target, target);
29
+ if (this._callbacks.onSubscribe) this._callbacks.onSubscribe(target);
30
+ }
31
+
32
+ unsubscribe(target) {
33
+ this._subscribers.delete(target);
34
+ if (this._callbacks.onUnsubscribe) this._callbacks.onUnsubscribe(target);
35
+ }
36
+
37
+ updateSubscribers() {
38
+ if (!this._subscribers || this._subscribers.size === 0) return;
39
+ if (!this._callbacks.updateSubscribers) return;
40
+
41
+ // debounce the updates
42
+ if (this._updateSubscribersRequested) return;
43
+
44
+ this._updateSubscribersRequested = true;
45
+ setTimeout(() => {
46
+ this._callbacks.updateSubscribers(this._subscribers);
47
+ this._updateSubscribersRequested = false;
48
+ }, 0);
49
+ }
50
+
51
+ _handleSubscribe(e) {
52
+ e.stopPropagation();
53
+ e.detail.registry = this._host;
54
+ const target = e.composedPath()[0];
55
+ this.subscribe(target);
56
+ }
57
+ }
58
+
59
+ export class EventSubscriberController {
60
+
61
+ constructor(host, callbacks, options) {
62
+ this._host = host;
63
+ this._callbacks = callbacks || {};
64
+ this._eventName = options && options.eventName;
65
+ this._controllerId = options && options.controllerId;
66
+ this._registry = null;
67
+ }
68
+
69
+ get registry() {
70
+ return this._registry;
71
+ }
72
+
73
+ hostConnected() {
74
+ // delay subscription otherwise import/upgrade order can cause selection mixin to miss event
75
+ requestAnimationFrame(() => {
76
+ const evt = new CustomEvent(this._eventName, {
77
+ bubbles: true,
78
+ composed: true,
79
+ detail: {}
80
+ });
81
+ this._host.dispatchEvent(evt);
82
+ this._registry = evt.detail.registry;
83
+
84
+ if (!this._registry) {
85
+ if (this._callbacks.onError) this._callbacks.onError();
86
+ return;
87
+ }
88
+ if (this._callbacks.onSubscribe) this._callbacks.onSubscribe(this._registry);
89
+ });
90
+ }
91
+
92
+ hostDisconnected() {
93
+ if (this._registry) this._registry.getController(this._controllerId).unsubscribe(this._host);
94
+ }
95
+
96
+ }
97
+
98
+ export class IdSubscriberController {
99
+
100
+ constructor(host, callbacks, options) {
101
+ this._host = host;
102
+ this._callbacks = callbacks || {};
103
+ this._idPropertyName = options && options.idPropertyName;
104
+ this._controllerId = options && options.controllerId;
105
+ this._registries = new Map();
106
+ this._timeouts = new Set();
107
+ }
108
+
109
+ get registries() {
110
+ return Array.from(this._registries.values());
111
+ }
112
+
113
+ hostDisconnected() {
114
+ if (this._registryObserver) this._registryObserver.disconnect();
115
+ this._timeouts.forEach(timeoutId => clearTimeout(timeoutId));
116
+ this._registries.forEach(registry => {
117
+ registry.getController(this._controllerId).unsubscribe(this._host);
118
+ });
119
+ }
120
+
121
+ hostUpdated(changedProperties) {
122
+ if (!changedProperties.has(this._idPropertyName)) return;
123
+
124
+ if (this._registryObserver) this._registryObserver.disconnect();
125
+ this._registries.forEach(registry => {
126
+ registry.getController(this._controllerId).unsubscribe(this._host);
127
+ if (this._callbacks.onUnsubscribe) this._callbacks.onUnsubscribe(registry.id);
128
+ });
129
+ this._registries = new Map();
130
+
131
+ this._updateRegistries();
132
+
133
+ this._registryObserver = new MutationObserver(() => {
134
+ this._updateRegistries();
135
+ });
136
+
137
+ this._registryObserver.observe(this._host.getRootNode(), {
138
+ childList: true,
139
+ subtree: true
140
+ });
141
+ }
142
+
143
+ _updateRegistries() {
144
+ let registryIds = this._host[this._idPropertyName];
145
+ if (!registryIds) return;
146
+
147
+ registryIds = registryIds.split(' ');
148
+ registryIds.forEach(registryId => {
149
+ this._updateRegistry(registryId, 0);
150
+ });
151
+ }
152
+
153
+ _updateRegistry(registryId, elapsedTime) {
154
+ let registryComponent = this._host.getRootNode().querySelector(`#${cssEscape(registryId)}`);
155
+ if (!registryComponent && this._callbacks.onError) {
156
+ if (elapsedTime < 3000) {
157
+ const timeoutId = setTimeout(() => {
158
+ this._timeouts.delete(timeoutId);
159
+ this._updateRegistry(registryId, elapsedTime + 100);
160
+ }, 100);
161
+ this._timeouts.add(timeoutId);
162
+ } else {
163
+ this._callbacks.onError(registryId);
164
+ }
165
+ }
166
+
167
+ registryComponent = registryComponent || undefined;
168
+ if (this._registries.get(registryId) === registryComponent) return;
169
+
170
+ if (registryComponent) {
171
+ registryComponent.getController(this._controllerId).subscribe(this._host);
172
+ this._registries.set(registryId, registryComponent);
173
+ if (this._callbacks.onSubscribe) this._callbacks.onSubscribe(registryComponent);
174
+ } else {
175
+ this._registries.delete(registryId);
176
+ if (this._callbacks.onUnsubscribe) this._callbacks.onUnsubscribe(registryId);
177
+ }
178
+ }
179
+
180
+ }