@brightspace-ui/labs 2.18.0 → 2.19.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.
@@ -0,0 +1,234 @@
1
+ import '@brightspace-ui/core/components/icons/icon.js';
2
+ import '@brightspace-ui/core/components/inputs/input-checkbox';
3
+
4
+ import { css, html, LitElement, nothing } from 'lit';
5
+ import { LocalizeLabsElement } from '../localize-labs-element.js';
6
+ import { RtlMixin } from '@brightspace-ui/core/mixins/rtl-mixin.js';
7
+
8
+ /**
9
+ * @property {string} name
10
+ * @property {Number} dataId - returned in event.detail; caller should use this to update its model
11
+ * @property {boolean} isOpen - whether the node is expanded (i.e. children are hidden unless true)
12
+ * @property {string} selectedState - checkbox state: may be "explicit", "indeterminate", or "none"
13
+ * @fires d2l-labs-tree-selector-node-select - user is requesting that this node be selected or deselected
14
+ * @fires d2l-labs-tree-selector-node-open - user has requested that this node be expanded or collapsed
15
+ *
16
+ */
17
+ class TreeSelectorNode extends LocalizeLabsElement(RtlMixin(LitElement)) {
18
+ static get properties() {
19
+ return {
20
+ name: { type: String },
21
+ dataId: { type: Number, attribute: 'data-id' },
22
+ isOpen: { type: Boolean, reflect: true, attribute: 'open' },
23
+ selectedState: { type: String, reflect: true, attribute: 'selected-state' },
24
+ isOpenable: { type: Boolean, reflect: true, attribute: 'openable' },
25
+ // for screen readers
26
+ indentLevel: { type: Number, attribute: 'indent-level' },
27
+ parentName: { type: String, attribute: 'parent-name' },
28
+ // for search: if isSearch, only search-result nodes are shown; the caller should ensure their ancestors are open
29
+ isSearch: { type: Boolean, reflect: true, attribute: 'search' },
30
+ isSearchResult: { type: Boolean, reflect: true, attribute: 'search-result' }
31
+ };
32
+ }
33
+
34
+ static get styles() {
35
+ return css`
36
+ :host {
37
+ display: block;
38
+ font-size: 0.8rem;
39
+ }
40
+ :host([hidden]) {
41
+ display: none;
42
+ }
43
+
44
+ .d2l-labs-tree-selector-node-node {
45
+ display: flex;
46
+ flex-wrap: nowrap;
47
+ margin-bottom: 16px;
48
+ }
49
+
50
+ d2l-input-checkbox {
51
+ display: inline-block;
52
+ }
53
+
54
+ .d2l-labs-tree-selector-node-open-control {
55
+ cursor: default;
56
+ margin-top: -3px;
57
+ }
58
+ .d2l-labs-tree-selector-node-open-control .d2l-labs-tree-selector-node-open {
59
+ display: none;
60
+ }
61
+ .d2l-labs-tree-selector-node-open-control[open] .d2l-labs-tree-selector-node-open {
62
+ display: inline-block;
63
+ }
64
+ .d2l-labs-tree-selector-node-open-control[open] .d2l-labs-tree-selector-node-closed {
65
+ display: none;
66
+ }
67
+
68
+ .d2l-labs-tree-selector-node-subtree {
69
+ margin-left: 34px;
70
+ margin-right: 0;
71
+ }
72
+ :host([dir="rtl"]) .d2l-labs-tree-selector-node-subtree {
73
+ margin-left: 0;
74
+ margin-right: 34px;
75
+ }
76
+ .d2l-labs-tree-selector-node-subtree[hidden] {
77
+ display: none;
78
+ }
79
+
80
+ .d2l-labs-tree-selector-node-text {
81
+ cursor: default;
82
+ display: inline-block;
83
+ margin-left: 0.5rem;
84
+ margin-right: 0.5rem;
85
+ width: 100%;
86
+ }
87
+ `;
88
+ }
89
+
90
+ constructor() {
91
+ super();
92
+
93
+ this.isOpen = false;
94
+ this.selectedState = 'none';
95
+ this.indentLevel = 0;
96
+ }
97
+
98
+ /**
99
+ * @returns {Promise} - resolves when all tree-selector-nodes in slots, recursively, have finished updating
100
+ */
101
+ get treeUpdateComplete() {
102
+ return this._waitForTreeUpdateComplete();
103
+ }
104
+
105
+ render() {
106
+ return html`
107
+ ${this._renderNode()}
108
+ ${this._renderSubtree()}
109
+ `;
110
+ }
111
+
112
+ simulateArrowClick() {
113
+ this._onArrowClick();
114
+ }
115
+
116
+ simulateCheckboxClick() {
117
+ this.shadowRoot?.querySelector('d2l-input-checkbox').simulateClick();
118
+ }
119
+
120
+ get _arrowLabel() {
121
+ return this.localize(
122
+ this.isOpen ?
123
+ 'components:ouFilter:treeSelector:arrowLabel:open' :
124
+ 'components:ouFilter:treeSelector:arrowLabel:closed',
125
+ { name: this.name, level: this.indentLevel, parentName: this.parentName }
126
+ );
127
+ }
128
+
129
+ get _showIndeterminate() {
130
+ return this.selectedState === 'indeterminate';
131
+ }
132
+
133
+ get _showSelected() {
134
+ return this.selectedState === 'explicit';
135
+ }
136
+
137
+ _onArrowClick() {
138
+ if (!this.isOpenable) return;
139
+
140
+ /**
141
+ * @event d2l-labs-tree-selector-node-open
142
+ */
143
+ this.dispatchEvent(new CustomEvent(
144
+ 'd2l-labs-tree-selector-node-open',
145
+ {
146
+ bubbles: true,
147
+ composed: false,
148
+ detail: {
149
+ id: this.dataId,
150
+ isOpen: !this.isOpen
151
+ }
152
+ }
153
+ ));
154
+ }
155
+
156
+ _onChange(e) {
157
+ /**
158
+ * @event d2l-labs-tree-selector-node-select
159
+ */
160
+ this.dispatchEvent(new CustomEvent(
161
+ 'd2l-labs-tree-selector-node-select',
162
+ {
163
+ bubbles: true,
164
+ composed: false,
165
+ detail: {
166
+ id: this.dataId,
167
+ isSelected: e.target.checked
168
+ }
169
+ }
170
+ ));
171
+ }
172
+
173
+ _renderNode() {
174
+ const label = this.parentName ?
175
+ this.localize('components:ouFilter:treeSelector:node:ariaLabel', { name: this.name, parentName: this.parentName }) :
176
+ this.name;
177
+
178
+ return html`
179
+ <div class="d2l-labs-tree-selector-node-node" ?search="${this.isSearch}" ?search-result="${this.isSearchResult}">
180
+ <d2l-input-checkbox
181
+ ?checked="${this._showSelected}"
182
+ ?indeterminate="${this._showIndeterminate}"
183
+ aria-label="${label}"
184
+ @change="${this._onChange}"
185
+ ></d2l-input-checkbox>
186
+ <span class="d2l-labs-tree-selector-node-text" @click="${this._onArrowClick}" aria-hidden="true">${this.name}</span>
187
+ ${this._renderOpenControl()}
188
+ </div>
189
+ `;
190
+ }
191
+
192
+ _renderOpenControl() {
193
+ if (this.isOpenable) {
194
+ return html`
195
+ <a href="#" class="d2l-labs-tree-selector-node-open-control"
196
+ ?open="${this.isOpen}"
197
+ @click="${this._onArrowClick}"
198
+ aria-label="${this._arrowLabel}"
199
+ aria-expanded="${this.isOpen}"
200
+ >
201
+ <d2l-icon class="d2l-labs-tree-selector-node-closed" icon="tier1:arrow-expand"></d2l-icon>
202
+ <d2l-icon class="d2l-labs-tree-selector-node-open" icon="tier1:arrow-collapse"></d2l-icon>
203
+ </a>
204
+ `;
205
+ } else {
206
+ return html`<span class="no-open-control"></span>`;
207
+ }
208
+ }
209
+
210
+ _renderSubtree() {
211
+ if (this.isOpenable) {
212
+ return html`<div class="d2l-labs-tree-selector-node-subtree"
213
+ ?hidden="${!this.isOpen}"
214
+ id="subtree"
215
+ >
216
+ <slot name="tree"></slot>
217
+ </div>`;
218
+ } else {
219
+ return nothing;
220
+ }
221
+ }
222
+
223
+ async _waitForTreeUpdateComplete() {
224
+ await this.updateComplete;
225
+ const slot = this.shadowRoot?.querySelector('slot');
226
+ // to be sure all child nodes have been added, instead of using flatten,
227
+ // we recursively walk down the tree, waiting for each node's update to complete
228
+ if (slot) {
229
+ const childNodes = slot.assignedNodes({ flatten: false });
230
+ return Promise.all(childNodes.map(node => node.treeUpdateComplete));
231
+ }
232
+ }
233
+ }
234
+ customElements.define('d2l-labs-tree-selector-node', TreeSelectorNode);
@@ -0,0 +1,250 @@
1
+ import './tree-selector-node.js';
2
+ import '@brightspace-ui/core/components/button/button-subtle.js';
3
+ import '@brightspace-ui/core/components/dropdown/dropdown-button-subtle.js';
4
+ import '@brightspace-ui/core/components/dropdown/dropdown-content.js';
5
+ import '@brightspace-ui/core/components/dropdown/dropdown.js';
6
+ import '@brightspace-ui/core/components/inputs/input-search.js';
7
+
8
+ import { css, html, LitElement, nothing } from 'lit';
9
+ import { classMap } from 'lit/directives/class-map.js';
10
+ import { LocalizeLabsElement } from '../localize-labs-element.js';
11
+ import { selectStyles } from '@brightspace-ui/core/components/inputs/input-select-styles';
12
+
13
+ /**
14
+ * @property {String} name
15
+ * @property {Boolean} isSearch - if true, show "search-results" slot instead of "tree" slot
16
+ * @property {Boolean} isSelected - if true, show a "Clear" button in the header
17
+ * @fires d2l-labs-tree-selector-search - user requested or cleared a search; search string is event.detail.value
18
+ * @fires d2l-labs-tree-selector-clear - user requested that all selections be cleared
19
+ * @fires d2l-labs-tree-selector-select-all - user requested that all nodes be checked
20
+ */
21
+ class TreeSelector extends LocalizeLabsElement(LitElement) {
22
+
23
+ static get properties() {
24
+ return {
25
+ name: { type: String },
26
+ disabled: { type: Boolean, attribute: 'disabled' },
27
+ isSelectAllVisible: { type: Boolean, attribute: 'select-all-ui', reflect: true },
28
+ isSearch: { type: Boolean, attribute: 'search', reflect: true },
29
+ isSelected: { type: Boolean, attribute: 'selected', reflect: true }
30
+ };
31
+ }
32
+
33
+ static get styles() {
34
+ return [
35
+ selectStyles,
36
+ css`
37
+ :host {
38
+ display: inline-block;
39
+ }
40
+ :host([hidden]) {
41
+ display: none;
42
+ }
43
+
44
+ .d2l-labs-filter-dropdown-content-header {
45
+ display: flex;
46
+ justify-content: start;
47
+ }
48
+ .d2l-labs-filter-dropdown-content-header > span {
49
+ align-self: center;
50
+ }
51
+
52
+ .d2l-labs-tree-selector-margin-button {
53
+ margin-inline-end: 6px;
54
+ }
55
+
56
+ .d2l-labs-tree-selector-margin-auto {
57
+ margin-inline-start: auto;
58
+ }
59
+
60
+ .d2l-labs-tree-selector-search {
61
+ display: flex;
62
+ flex-wrap: nowrap;
63
+ padding-bottom: 26px;
64
+ padding-top: 4px;
65
+ }
66
+ @media screen and (max-width: 400px) {
67
+ .d2l-labs-tree-selector-search {
68
+ width: 100%;
69
+ }
70
+ }
71
+ :host([search]) d2l-dropdown d2l-dropdown-content .d2l-labs-tree-selector-tree {
72
+ display: none;
73
+ }
74
+
75
+ .d2l-labs-tree-selector-search-results {
76
+ display: none;
77
+ }
78
+ :host([search]) d2l-dropdown d2l-dropdown-content .d2l-labs-tree-selector-search-results {
79
+ display: block;
80
+ }
81
+ `
82
+ ];
83
+ }
84
+
85
+ constructor() {
86
+ super();
87
+
88
+ this.name = 'SPECIFY NAME ATTRIBUTE';
89
+ this._isSearch = false;
90
+ this.isSelectAllVisible = false;
91
+ this.disabled = false;
92
+ }
93
+
94
+ /**
95
+ * @returns {Promise} - resolves when all tree-selector-nodes in slots, recursively, have finished updating
96
+ */
97
+ get treeUpdateComplete() {
98
+ return this._waitForTreeUpdateComplete();
99
+ }
100
+
101
+ render() {
102
+ // Using no-auto-fit on d2l-dropdown-content to avoid having the component jump to the top on every
103
+ // node open and load. The tradeoff is that the user has to scroll the whole page now.
104
+ // We have a defect logged to improve this in future.
105
+ return html`
106
+ <d2l-dropdown>
107
+ <d2l-dropdown-button-subtle text="${this.name}" ?disabled=${this.disabled}>
108
+ <d2l-dropdown-content align="start" no-auto-fit class="vdiff-target">
109
+ <div class="d2l-labs-filter-dropdown-content-header" slot="header">
110
+ <span>${this.localize('components:ouFilter:treeSelector:filterBy')}</span>
111
+
112
+ ${this._clearButton}
113
+
114
+ ${this._selectAllButton}
115
+ </div>
116
+ <div class="d2l-labs-tree-selector-search">
117
+ <d2l-input-search
118
+ label="${this.localize('components:ouFilter:treeSelector:searchLabel')}"
119
+ placeholder="${this.localize('components:ouFilter:treeSelector:searchPlaceholder')}"
120
+ @d2l-input-search-searched="${this._onSearch}"
121
+ ></d2l-input-search>
122
+ </div>
123
+ <div class="d2l-labs-tree-selector-search-results">
124
+ <slot name="search-results"></slot>
125
+ </div>
126
+ <div class="d2l-labs-tree-selector-tree">
127
+ <slot name="tree"></slot>
128
+ </div>
129
+ </d2l-dropdown-content>
130
+ </d2l-dropdown-button-subtle>
131
+ </d2l-dropdown>
132
+ `;
133
+ }
134
+
135
+ clearSearchAndSelection(generateEvent = true) {
136
+ this.shadowRoot.querySelector('d2l-input-search').value = '';
137
+ this._onSearch({
138
+ detail: {
139
+ value: ''
140
+ }
141
+ }, generateEvent);
142
+
143
+ this._onClear(generateEvent);
144
+ }
145
+
146
+ async resize() {
147
+ await this.treeUpdateComplete;
148
+ const content = this.shadowRoot?.querySelector('d2l-dropdown-content');
149
+ content && await content.resize();
150
+ }
151
+
152
+ simulateSearch(searchString) {
153
+ this._onSearch({
154
+ detail: {
155
+ value: searchString
156
+ }
157
+ });
158
+ }
159
+
160
+ get _clearButton() {
161
+ if (!this.isSelected) return nothing;
162
+
163
+ const styles = {
164
+ 'd2l-labs-tree-selector-select-clear': true,
165
+ 'd2l-labs-tree-selector-margin-button': true,
166
+ 'd2l-labs-tree-selector-margin-auto': true
167
+ };
168
+
169
+ return html`
170
+ <d2l-button-subtle
171
+ class="${classMap(styles)}"
172
+ text="${this.localize('components:ouFilter:treeSelector:clearLabel')}"
173
+ @click="${this._onClear}"
174
+ ></d2l-button-subtle>`;
175
+ }
176
+
177
+ get _selectAllButton() {
178
+ if (!this.isSelectAllVisible) return nothing;
179
+
180
+ const styles = {
181
+ 'd2l-labs-tree-selector-select-all': true,
182
+ 'd2l-labs-tree-selector-margin-auto': !this.isSelected
183
+ };
184
+
185
+ return html`
186
+ <d2l-button-subtle
187
+ class="${classMap(styles)}"
188
+ text="${this.localize('components:ouFilter:treeSelector:selectAllLabel')}"
189
+ @click="${this._onSelectAll}"
190
+ ></d2l-button-subtle>`;
191
+ }
192
+
193
+ _onClear(generateEvent = true) {
194
+ if (!generateEvent) {
195
+ return;
196
+ }
197
+ /**
198
+ * @event d2l-labs-tree-selector-clear
199
+ */
200
+ this.dispatchEvent(new CustomEvent(
201
+ 'd2l-labs-tree-selector-clear',
202
+ {
203
+ bubbles: true,
204
+ composed: false
205
+ }
206
+ ));
207
+ }
208
+
209
+ _onSearch(event, generateEvent = true) {
210
+ if (!generateEvent) {
211
+ return;
212
+ }
213
+ /**
214
+ * @event d2l-labs-tree-selector-search
215
+ */
216
+ this.dispatchEvent(new CustomEvent(
217
+ 'd2l-labs-tree-selector-search',
218
+ {
219
+ bubbles: true,
220
+ composed: false,
221
+ detail: event.detail
222
+ }
223
+ ));
224
+ }
225
+
226
+ _onSelectAll() {
227
+ /**
228
+ * @event d2l-labs-tree-selector-select-all
229
+ */
230
+ this.dispatchEvent(new CustomEvent(
231
+ 'd2l-labs-tree-selector-select-all',
232
+ {
233
+ bubbles: true,
234
+ composed: false
235
+ }
236
+ ));
237
+ }
238
+
239
+ async _waitForTreeUpdateComplete() {
240
+ await this.updateComplete;
241
+ const slots = [...(this.shadowRoot?.querySelectorAll('slot') || [])];
242
+ // to be sure all child nodes have been added, instead of using flatten,
243
+ // we recursively walk down the tree, waiting for each node's update to complete
244
+ return Promise.all(slots.map(slot => {
245
+ const childNodes = slot.assignedNodes({ flatten: false });
246
+ return Promise.all(childNodes.map(node => node.treeUpdateComplete));
247
+ }));
248
+ }
249
+ }
250
+ customElements.define('d2l-labs-tree-selector', TreeSelector);