kemet-ui 1.2.0 → 1.3.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.
@@ -84,7 +84,7 @@ export default class KemetCombo extends LitElement {
84
84
  this.input = this.field.querySelector('[slot="input"]');
85
85
 
86
86
  // managed properties
87
- this.slug = this.field.slug;
87
+ this.slug = this.field.slug || 'slug';
88
88
 
89
89
  // events listeners
90
90
  this.input.addEventListener('kemet-input-input', this.handleInput.bind(this));
@@ -99,7 +99,7 @@ export default class KemetCombo extends LitElement {
99
99
  aria-controls="${this.slug}-listbox"
100
100
  aria-haspopup="listbox"
101
101
  id="${this.slug}-combobox"
102
- aria-label="${this.field.label}"
102
+ aria-label="${this.field?.label}"
103
103
  >
104
104
  <ul role="listbox" aria-labelledby="${this.slug}-label" id="${this.slug}-listbox">
105
105
  ${this.makeOptions()}
@@ -154,7 +154,11 @@ export default class KemetCombo extends LitElement {
154
154
  new CustomEvent('kemet-combo-selection', {
155
155
  bubbles: true,
156
156
  composed: true,
157
- detail: event.target.getAttribute('id'),
157
+ detail: {
158
+ element: event.target,
159
+ text: event.target.innerText,
160
+ id: event.target.getAttribute('id'),
161
+ },
158
162
  }),
159
163
  );
160
164
  }
@@ -75,7 +75,7 @@ export default class KemetModal extends LitElement {
75
75
  updated(prevProps) {
76
76
  if (!prevProps.get('opened') && this.opened === true) {
77
77
  setTimeout(() => {
78
- this.focusableElements[0].focus();
78
+ this.focusableElements[0]?.focus();
79
79
  }, 100);
80
80
 
81
81
  /**
@@ -0,0 +1,42 @@
1
+ import { html, css, LitElement } from 'lit';
2
+
3
+ export default class KemetSortableItem extends LitElement {
4
+ static get styles() {
5
+ return [
6
+ css`
7
+ :host {
8
+ cursor: grab;
9
+ display: block;
10
+ padding: 1rem;
11
+ border: 1px solid var(--kemet-color-primary);
12
+ }
13
+
14
+ :host([ghost]) {
15
+ opacity: 0.5;
16
+ border-style: dashed;
17
+ }
18
+ `,
19
+ ];
20
+ }
21
+
22
+ static get properties() {
23
+ return {
24
+ /* Automatically set to true when an item is dragged to a new spot */
25
+ ghost: {
26
+ type: Boolean,
27
+ reflect: true,
28
+ },
29
+ };
30
+ }
31
+
32
+ firstUpdated() {
33
+ this.draggable = true;
34
+ }
35
+
36
+ render() {
37
+ return html`<slot></slot>`;
38
+ }
39
+ }
40
+
41
+ // eslint-disable-next-line no-unused-expressions
42
+ customElements.get('kemet-sortable-item') || customElements.define('kemet-sortable-item', KemetSortableItem);
@@ -0,0 +1,93 @@
1
+ import { html, css, LitElement } from 'lit';
2
+ import { polyfill } from 'mobile-drag-drop';
3
+
4
+ const getMouseOffset = (event) => {
5
+ const targetRect = event.target.getBoundingClientRect();
6
+ const offset = {
7
+ x: event.pageX - targetRect.left,
8
+ y: event.pageY - targetRect.top,
9
+ };
10
+
11
+ return offset;
12
+ };
13
+
14
+ const getElementVerticalCenter = (element) => {
15
+ const rect = element.getBoundingClientRect();
16
+ return (rect.bottom - rect.top) / 2;
17
+ };
18
+
19
+ export default class KemetSortable extends LitElement {
20
+ static get styles() {
21
+ return [
22
+ css`
23
+ :host {
24
+ display: flex;
25
+ gap: 1rem;
26
+ flex-direction: column;
27
+ }
28
+ `,
29
+ ];
30
+ }
31
+
32
+ firstUpdated() {
33
+ polyfill();
34
+
35
+ this.addEventListener('dragstart', event => this.handleDragStart(event), false);
36
+ this.addEventListener('dragenter', (event) => { event.preventDefault(); });
37
+ }
38
+
39
+ render() {
40
+ return html`<slot></slot>`;
41
+ }
42
+
43
+ handleDragStart(event) {
44
+ this.sortableItem = event.target;
45
+
46
+ this.addEventListener('dragover', dragOverEvent => this.handleDragOver(dragOverEvent), false);
47
+ this.addEventListener('dragend', dragEndEvent => this.handleDragEnd(dragEndEvent), false);
48
+
49
+ setTimeout(() => {
50
+ this.sortableItem.ghost = true;
51
+ }, 0);
52
+ }
53
+
54
+ handleDragOver(event) {
55
+ event.preventDefault();
56
+
57
+ const { target } = event;
58
+
59
+ if (target && target.tagName === 'KEMET-SORTABLE-ITEM') {
60
+ const offset = getMouseOffset(event);
61
+ const middleY = getElementVerticalCenter(event.target);
62
+
63
+ if (offset.y > middleY) {
64
+ this.insertBefore(this.sortableItem, target.nextSibling);
65
+ } else {
66
+ this.insertBefore(this.sortableItem, target);
67
+ }
68
+ }
69
+ }
70
+
71
+ handleDragEnd(event) {
72
+ event.preventDefault();
73
+ this.sortableItem.ghost = false;
74
+
75
+ /**
76
+ * Fires when an item has been moved to a new spot
77
+ */
78
+ this.dispatchEvent(
79
+ new CustomEvent('kemet-sortable-drag-end', {
80
+ bubbles: true,
81
+ composed: true,
82
+ detail: {
83
+ event,
84
+ current: this.sortableItem,
85
+ all: this.querySelectorAll('kemet-sortable-item'),
86
+ },
87
+ }),
88
+ );
89
+ }
90
+ }
91
+
92
+ // eslint-disable-next-line no-unused-expressions
93
+ customElements.get('kemet-sortable') || customElements.define('kemet-sortable', KemetSortable);
@@ -0,0 +1,167 @@
1
+ import { html, css, LitElement } from 'lit';
2
+ import { stylesLoaders } from './kemet-upload-file.loaders.styles.js';
3
+
4
+ const formatBytes = (bytes, decimals = 2) => {
5
+ if (bytes === 0) return '0 Bytes';
6
+
7
+ const k = 1024;
8
+ const dm = decimals < 0 ? 0 : decimals;
9
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
10
+
11
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
12
+
13
+ // eslint-disable-next-line no-restricted-properties
14
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
15
+ };
16
+
17
+ export default class KemetUploadFile extends LitElement {
18
+ static get styles() {
19
+ return [
20
+ css`
21
+ :host {
22
+ --kemet-upload-file-ripple-color: var(--kemet-color-primary);
23
+
24
+ color: var(--kemet-upload-file-color, var(--kemet-color-gray7));
25
+ display: grid;
26
+ gap: 1rem;
27
+ grid-template-columns: auto 1fr auto;
28
+ align-items: center;
29
+ padding: var(--kemet-upload-file-padding, 0.5rem 1rem);
30
+ border: var(--kemet-upload-file-border, 1px solid var(--kemet-color-primary));
31
+ }
32
+
33
+ :host([status='error']) {
34
+ border: 1px solid var(--kemet-color-error);
35
+ }
36
+
37
+ :host([status='complete']) {
38
+ border: 1px solid var(--kemet-color-success);
39
+ }
40
+
41
+ h3 {
42
+ margin: 0;
43
+ }
44
+
45
+ .percentage {
46
+ font-size: clamp(2rem, 3vw, 2.5rem);
47
+ align-self: center;
48
+ justify-self: center;
49
+ }
50
+
51
+ .indicator {
52
+ transform: scale(0.8);
53
+ }
54
+
55
+ :host([status='complete']) .message,
56
+ :host([status='complete']) .indicator {
57
+ color: var(--kemet-color-success);
58
+ }
59
+
60
+ :host([status='error']) .message,
61
+ :host([status='error']) .indicator {
62
+ color: var(--kemet-color-error);
63
+ }
64
+ `,
65
+ stylesLoaders,
66
+ ];
67
+ }
68
+
69
+ static get properties() {
70
+ return {
71
+ /**
72
+ * The name of the file
73
+ */
74
+ name: {
75
+ type: String,
76
+ },
77
+ /**
78
+ * The number of bytes loaded
79
+ */
80
+ loaded: {
81
+ type: Number,
82
+ },
83
+ /**
84
+ * The file size in bytes
85
+ */
86
+ size: {
87
+ type: Number,
88
+ },
89
+ /**
90
+ * The file type
91
+ */
92
+ type: {
93
+ type: String,
94
+ },
95
+ /**
96
+ * The status of the file. Values are complete | uploading | error.
97
+ */
98
+ status: {
99
+ type: String,
100
+ reflect: true,
101
+ },
102
+ /**
103
+ * A percentage of file completion that's calculated by the loaded property
104
+ */
105
+ percent: {
106
+ type: Number,
107
+ },
108
+ /**
109
+ * An error message to give to users
110
+ */
111
+ message: {
112
+ type: String,
113
+ },
114
+ };
115
+ }
116
+
117
+ constructor() {
118
+ super();
119
+ this.loaded = 0;
120
+ this.size = 0;
121
+ this.percent = 0;
122
+ }
123
+
124
+ updated() {
125
+ this.percent = this.calculatePercent();
126
+ }
127
+
128
+ render() {
129
+ return html`
130
+ <div class="percentage" part="percentage">
131
+ <span>${this.percent}%</span>
132
+ </div>
133
+ <div>
134
+ <h3 part="filename">${this.name}</h3>
135
+ <span part="loaded">${formatBytes(this.loaded)} / ${formatBytes(this.size)}</span>
136
+ ${this.message ? html`<div class="message" part="message">${this.message}</div>` : null}
137
+ </div>
138
+ <div class="indicator" part="indicator">
139
+ ${this.getStatusIcon()}
140
+ </div>
141
+ `;
142
+ }
143
+
144
+ calculatePercent() {
145
+ const percent = Math.round((this.loaded * 100.0) / this.size);
146
+ return percent > 100 ? 100 : percent;
147
+ }
148
+
149
+ getStatusIcon() {
150
+ if (this.status === 'uploading') {
151
+ return html`<div class="lds-ripple"><div></div><div></div></div>`;
152
+ }
153
+
154
+ if (this.status === 'complete') {
155
+ return html`<kemet-icon icon="check-circle" size="48"></kemet-icon>`;
156
+ }
157
+
158
+ if (this.status === 'error') {
159
+ return html`<kemet-icon icon="exclamation-circle" size="48"></kemet-icon>`;
160
+ }
161
+
162
+ return null;
163
+ }
164
+ }
165
+
166
+ // eslint-disable-next-line no-unused-expressions
167
+ customElements.get('kemet-upload-file') || customElements.define('kemet-upload-file', KemetUploadFile);
@@ -0,0 +1,53 @@
1
+ import { css } from 'lit';
2
+
3
+ export const stylesLoaders = css`
4
+ .lds-ripple {
5
+ display: inline-block;
6
+ position: relative;
7
+ width: 80px;
8
+ height: 80px;
9
+ }
10
+
11
+ .lds-ripple div {
12
+ position: absolute;
13
+ border: 4px solid var(--kemet-upload-file-ripple-color);
14
+ opacity: 1;
15
+ border-radius: 50%;
16
+ animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
17
+ }
18
+
19
+ .lds-ripple div:nth-child(2) {
20
+ animation-delay: -0.5s;
21
+ }
22
+
23
+ @keyframes lds-ripple {
24
+ 0% {
25
+ top: 36px;
26
+ left: 36px;
27
+ width: 0;
28
+ height: 0;
29
+ opacity: 0;
30
+ }
31
+ 4.9% {
32
+ top: 36px;
33
+ left: 36px;
34
+ width: 0;
35
+ height: 0;
36
+ opacity: 0;
37
+ }
38
+ 5% {
39
+ top: 36px;
40
+ left: 36px;
41
+ width: 0;
42
+ height: 0;
43
+ opacity: 1;
44
+ }
45
+ 100% {
46
+ top: 0px;
47
+ left: 0px;
48
+ width: 72px;
49
+ height: 72px;
50
+ opacity: 0;
51
+ }
52
+ }
53
+ `;
@@ -0,0 +1,260 @@
1
+ import { html, css, LitElement } from 'lit';
2
+ import { ifDefined } from 'lit/directives/if-defined.js';
3
+
4
+ const preventDefaults = (event) => {
5
+ event.preventDefault();
6
+ event.stopPropagation();
7
+ };
8
+
9
+ export default class KemetUpload extends LitElement {
10
+ static get styles() {
11
+ return [
12
+ css`
13
+ :host {
14
+ color: var(--kemet-upload-color, var(--kemet-color-white));
15
+ display: grid;
16
+ grid-template-columns: 1fr 1fr;
17
+ height: var(--kemet-upload-height, 364px);
18
+ border: var(--kemet-upload-border, 1rem solid var(--kemet-color-white));
19
+ background-color: var(--kemet-upload-background-color, var(--kemet-color-primary));
20
+ }
21
+
22
+ :host([mobile]) {
23
+ display: block;
24
+ height: auto;
25
+ }
26
+
27
+ input {
28
+ display: none;
29
+ }
30
+
31
+ .button {
32
+ cursor: pointer;
33
+ font-size: 1rem;
34
+ display: block;
35
+ color: var(--kemet-color-white);
36
+ padding: 0.5rem 1rem;
37
+ border: 1px solid var(--kemet-color-white);
38
+ background-color: transparent;
39
+ }
40
+
41
+ .upload {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ margin: 1rem;
46
+ flex-direction: column;
47
+ border: 2px dashed var(--kemet-color-white);
48
+ }
49
+
50
+ :host([over]) .upload {
51
+ background-color: green;
52
+ }
53
+
54
+ :host([mobile]) .upload {
55
+ height: 280px;
56
+ }
57
+
58
+ .files {
59
+ display: flex;
60
+ gap: 1rem;
61
+ flex-direction: column;
62
+ padding: 0 1rem;
63
+ overflow: auto;
64
+ background-color: var(--kemet-color-white);
65
+ }
66
+
67
+ :host([mobile]) .files {
68
+ padding: 0;
69
+ max-height: 280px;
70
+ }
71
+
72
+ .heading {
73
+ font-size: 1.25rem;
74
+ margin: 0 0 2rem 0;
75
+ }
76
+
77
+ :host([no-drag-drop]) .heading {
78
+ display: none;
79
+ }
80
+
81
+ ::slotted(:first-child) {
82
+ margin-top: 1rem;
83
+ }
84
+ `,
85
+ ];
86
+ }
87
+
88
+ static get properties() {
89
+ return {
90
+ /**
91
+ * A unique identifier for the component
92
+ */
93
+ slug: {
94
+ type: String,
95
+ },
96
+ /**
97
+ * Determines what file types are accepted
98
+ */
99
+ accept: {
100
+ type: String,
101
+ },
102
+ /**
103
+ * Allows for multiple files
104
+ */
105
+ multiple: {
106
+ type: Boolean,
107
+ },
108
+ /**
109
+ * Automatically is true when a file is being dragged over the upload area
110
+ */
111
+ over: {
112
+ type: Boolean,
113
+ reflect: true,
114
+ },
115
+ /**
116
+ * Descriptive text for the upload area
117
+ */
118
+ heading: {
119
+ type: String,
120
+ },
121
+ /**
122
+ * Displays the component in a mobile context
123
+ */
124
+ mobile: {
125
+ type: Boolean,
126
+ reflect: true,
127
+ },
128
+ /**
129
+ * Controls the point at which the component is considered mobile
130
+ */
131
+ breakpoint: {
132
+ type: String,
133
+ reflect: true,
134
+ },
135
+ /**
136
+ * If true if drag and drop support is not detected
137
+ */
138
+ noDragDrop: {
139
+ type: Boolean,
140
+ reflect: true,
141
+ attribute: 'no-drag-drop',
142
+ },
143
+ /**
144
+ * The maximum file size for uploads
145
+ */
146
+ maxSize: {
147
+ type: Number,
148
+ attribute: 'max-size',
149
+ },
150
+ /**
151
+ * The browse files button text
152
+ */
153
+ buttonLabel: {
154
+ type: String,
155
+ attribute: 'button-label',
156
+ },
157
+ };
158
+ }
159
+
160
+ constructor() {
161
+ super();
162
+ this.slug = 'upload';
163
+ this.breakpoint = '600px';
164
+ this.buttonLabel = 'Browse Files';
165
+ this.heading = 'Drag & Drop Files';
166
+ }
167
+
168
+ firstUpdated() {
169
+ this.fileInputElement = this.shadowRoot.querySelector('[type="file"]');
170
+ this.upload = this.shadowRoot.querySelector('.upload');
171
+
172
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
173
+ this.upload.addEventListener(eventName, event => preventDefaults(event), false);
174
+ });
175
+
176
+ ['dragenter', 'dragover'].forEach((eventName) => {
177
+ this.upload.addEventListener(eventName, () => { this.over = true; }, false);
178
+ });
179
+
180
+ ['dragleave', 'drop'].forEach((eventName) => {
181
+ this.upload.addEventListener(eventName, () => { this.over = false; }, false);
182
+ });
183
+
184
+ this.isMobile();
185
+ this.hasDragDrop();
186
+
187
+ window.addEventListener('resize', () => { this.isMobile(); });
188
+ this.upload.addEventListener('drop', event => this.handleDrop(event), false);
189
+ }
190
+
191
+ render() {
192
+ return html`
193
+ <div class="upload" part="upload">
194
+ <kemet-icon icon="cloud-arrow-up" size="128"></kemet-icon>
195
+ <input
196
+ type="file"
197
+ id=${this.slug}
198
+ ?multiple=${this.multiple}
199
+ accept=${ifDefined(this.accept) ? this.accept : null}
200
+ @change=${event => this.handleChange(event)}
201
+ />
202
+ <h3 class="heading" part="heading">${this.heading}</h3>
203
+ <label class="button" part="button" for=${this.slug}>${this.buttonLabel}</label>
204
+ </div>
205
+ <div class="files" part="files">
206
+ <slot></slot>
207
+ </div>
208
+ `;
209
+ }
210
+
211
+ handleChange(event) {
212
+ /**
213
+ * Fires when files have been added
214
+ */
215
+ this.dispatchEvent(
216
+ new CustomEvent('kemet-upload-change', {
217
+ bubbles: true,
218
+ composed: true,
219
+ detail: {
220
+ event,
221
+ files: this.fileInputElement.files,
222
+ fileElement: this.fileInputElement,
223
+ },
224
+ }),
225
+ );
226
+ }
227
+
228
+ handleDrop(event) {
229
+ /**
230
+ * Fires when files have been added
231
+ */
232
+ this.dispatchEvent(
233
+ new CustomEvent('kemet-upload-change', {
234
+ bubbles: true,
235
+ composed: true,
236
+ detail: {
237
+ event,
238
+ files: event?.dataTransfer.files || [],
239
+ fileElement: this.fileInputElement,
240
+ },
241
+ }),
242
+ );
243
+ }
244
+
245
+ isMobile() {
246
+ const mediaQuery = window.matchMedia(`(max-width: ${this.breakpoint})`);
247
+ this.mobile = mediaQuery.matches;
248
+ }
249
+
250
+ hasDragDrop() {
251
+ if ('draggable' in document.createElement('span')) {
252
+ this.noDragDrop = false;
253
+ } else {
254
+ this.noDragDrop = true;
255
+ }
256
+ }
257
+ }
258
+
259
+ // eslint-disable-next-line no-unused-expressions
260
+ customElements.get('kemet-upload') || customElements.define('kemet-upload', KemetUpload);