@studiocms/ui 0.1.0 → 0.3.1

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.
@@ -2,21 +2,63 @@
2
2
  import Icon from '../utils/Icon.astro';
3
3
  import { generateID } from '../utils/generateID';
4
4
 
5
+ /**
6
+ * The props for the select component.
7
+ */
5
8
  interface Option {
9
+ /**
10
+ * The label of the option.
11
+ */
6
12
  label: string;
13
+ /**
14
+ * The value of the option.
15
+ */
7
16
  value: string;
17
+ /**
18
+ * Whether the option is disabled. Defaults to `false`.
19
+ */
8
20
  disabled?: boolean;
9
21
  }
10
22
 
23
+ /**
24
+ * The props for the select component.
25
+ */
11
26
  interface Props {
27
+ /**
28
+ * The label of the select.
29
+ */
12
30
  label?: string;
31
+ /**
32
+ * The default value of the select.
33
+ */
13
34
  defaultValue?: string;
35
+ /**
36
+ * Additional classes to apply to the select.
37
+ */
14
38
  class?: string;
39
+ /**
40
+ * The name of the select. Required because of the helper.
41
+ */
15
42
  name?: string;
43
+ /**
44
+ * Whether the select is required. Defaults to `false`.
45
+ */
16
46
  isRequired?: boolean;
47
+ /**
48
+ * The options to display in the select.
49
+ */
17
50
  options: Option[];
51
+ /**
52
+ * Whether the select is disabled. Defaults to `false`.
53
+ */
18
54
  disabled?: boolean;
55
+ /**
56
+ * Whether the select is full width. Defaults to `false`.
57
+ */
19
58
  fullWidth?: boolean;
59
+ /**
60
+ * The placeholder of the select. Defaults to `Select`.
61
+ */
20
62
  placeholder?: string;
21
63
  }
22
64
 
@@ -29,7 +71,7 @@ const {
29
71
  options = [],
30
72
  disabled,
31
73
  fullWidth,
32
- placeholder,
74
+ placeholder = 'Select',
33
75
  } = Astro.props;
34
76
  ---
35
77
 
@@ -37,6 +79,8 @@ const {
37
79
  id={`${name}-container`}
38
80
  class="sui-select-label"
39
81
  class:list={[disabled && "disabled", className, fullWidth && "full"]}
82
+ data-options={JSON.stringify(options)}
83
+ data-id={name}
40
84
  >
41
85
  {label && (
42
86
  <label class="label" for={`${name}-select-btn`}>
@@ -51,12 +95,14 @@ const {
51
95
  aria-expanded="false"
52
96
  id={`${name}-select-btn`}
53
97
  type="button"
98
+ aria-label={placeholder}
99
+ title={placeholder}
54
100
  >
55
- <span id={`${name}-value-span`}>
101
+ <span class="sui-select-value-span" id={`${name}-value-span`}>
56
102
  {
57
103
  defaultValue
58
104
  ? options.find((x) => x.value === defaultValue)?.label
59
- : placeholder || "Select"
105
+ : placeholder
60
106
  }
61
107
  </span>
62
108
  <Icon name="chevron-up-down" width={24} height={24} />
@@ -74,14 +120,13 @@ const {
74
120
  ]}
75
121
  id={defaultValue === x.value ? `${name}-selected` : ""}
76
122
  data-option-index={i}
77
- tabindex={x.disabled ? -1 : 0}
78
123
  >
79
124
  {x.label}
80
125
  </li>
81
126
  ))
82
127
  }
83
128
  </ul>
84
- <select class="sui-hidden-select" id={name} name={name} required={isRequired}>
129
+ <select class="sui-hidden-select" id={name} name={name} required={isRequired} hidden tabindex="-1">
85
130
  <option value={""}> Select </option>
86
131
  {
87
132
  options.map((x) => (
@@ -96,129 +141,204 @@ const {
96
141
  }
97
142
  </select>
98
143
  </div>
99
- <script is:inline define:vars={{ id: name, options }}>
100
- const container = document.getElementById(`${id}-container`);
101
- const hiddenSelect = document.getElementById(id);
102
- const button = document.getElementById(`${id}-select-btn`);
103
- const valueSpan = document.getElementById(`${id}-value-span`);
104
- const dropdown = document.getElementById(`${id}-dropdown`);
105
- const optionElements = container.querySelectorAll("li");
106
-
107
- let active = false;
108
-
109
- button.addEventListener("click", () => {
110
- const { bottom, left, right, width, x, y, height } =
111
- button.getBoundingClientRect();
112
-
113
- const optionHeight = 36;
114
- const totalBorderSize = 2;
115
- const margin = 4;
116
-
117
- const dropdownHeight =
118
- options.length * optionHeight + totalBorderSize + margin;
119
-
120
- const CustomRect = {
121
- top: bottom + margin,
122
- left,
123
- right,
124
- bottom: bottom + margin + dropdownHeight,
125
- width,
126
- height: dropdownHeight,
127
- x,
128
- y: y + height + margin,
129
- };
130
-
131
- if (active) {
132
- button.ariaExpanded = false;
144
+ <script>
145
+ const allSelects = document.querySelectorAll<HTMLDivElement>(".sui-select-label");
146
+ // id, options
147
+
148
+ for (const container of allSelects) {
149
+ const hiddenSelect = container.querySelector('select')!;
150
+ const button = container.querySelector('button')!;
151
+ const valueSpan = container.querySelector('.sui-select-value-span')!;
152
+ const dropdown = container.querySelector('.sui-select-dropdown')!;
153
+ const optionElements = container.querySelectorAll<HTMLLIElement>('.sui-select-option');
154
+
155
+ const options = JSON.parse(container.dataset.options!);
156
+ const id = container.dataset.id!;
157
+ let active = false;
158
+
159
+ const closeDropdown = () => {
133
160
  dropdown.classList.remove("active", "above");
134
161
  active = false;
135
- return;
162
+ button.ariaExpanded = 'false';
163
+ focusIndex = -1;
164
+
165
+ for (const entry of optionElements) {
166
+ entry.classList.remove('focused');
167
+ }
168
+ };
169
+
170
+ const openDropdown = (toggle: boolean) => {
171
+ const { bottom, left, right, width, x, y, height } =
172
+ button.getBoundingClientRect();
173
+
174
+ const optionHeight = 36;
175
+ const totalBorderSize = 2;
176
+ const margin = 4;
177
+
178
+ const dropdownHeight =
179
+ options.length * optionHeight + totalBorderSize + margin;
180
+
181
+ const CustomRect = {
182
+ top: bottom + margin,
183
+ left,
184
+ right,
185
+ bottom: bottom + margin + dropdownHeight,
186
+ width,
187
+ height: dropdownHeight,
188
+ x,
189
+ y: y + height + margin,
190
+ };
191
+
192
+ if (active && toggle) {
193
+ closeDropdown();
194
+ return;
195
+ }
196
+
197
+ active = true;
198
+ button.ariaExpanded = 'true';
199
+
200
+ // Set focusIndex to currently selected option
201
+ focusIndex = Array.from(optionElements).findIndex((x) =>
202
+ x.classList.contains("selected")
203
+ );
204
+
205
+ if (
206
+ CustomRect.top >= 0 &&
207
+ CustomRect.left >= 0 &&
208
+ CustomRect.bottom <=
209
+ (window.innerHeight || document.documentElement.clientHeight) &&
210
+ CustomRect.right <=
211
+ (window.innerWidth || document.documentElement.clientWidth)
212
+ ) {
213
+ dropdown.classList.add("active");
214
+ } else {
215
+ dropdown.classList.add("active", "above");
216
+ }
136
217
  }
137
218
 
138
- active = true;
139
- button.ariaExpanded = true;
140
-
141
- if (
142
- CustomRect.top >= 0 &&
143
- CustomRect.left >= 0 &&
144
- CustomRect.bottom <=
145
- (window.innerHeight || document.documentElement.clientHeight) &&
146
- CustomRect.right <=
147
- (window.innerWidth || document.documentElement.clientWidth)
148
- ) {
149
- dropdown.classList.add("active");
150
- } else {
151
- dropdown.classList.add("active", "above");
219
+ button.addEventListener("click", () => openDropdown(true));
220
+
221
+ let focusIndex = -1;
222
+
223
+ const recomputeOptions = () => {
224
+ for (const entry of optionElements) {
225
+ if (Number.parseInt(entry.dataset.optionIndex!) == focusIndex) {
226
+ entry.classList.add('focused');
227
+ } else {
228
+ entry.classList.remove('focused');
229
+ }
230
+ }
152
231
  }
153
- });
154
232
 
155
- optionElements.forEach((option) => {
156
- const handleSelection = (e) => {
233
+ button.addEventListener('keydown', (e) => {
234
+ if (e.key === 'Tab' || e.key === 'Escape') {
235
+ closeDropdown();
236
+ return;
237
+ }
238
+
239
+ if (e.key === ' ' && !active) openDropdown(false);
240
+
241
+ if (e.key === 'Enter') {
242
+ let currentlyFocused = container.querySelector<HTMLElement>('.focused');
243
+ if (currentlyFocused) {
244
+ currentlyFocused.classList.remove('focused');
245
+ currentlyFocused.click();
246
+
247
+ // Stop dropdown from immediately reopening
248
+ e.preventDefault();
249
+ e.stopImmediatePropagation();
250
+ }
251
+
252
+ return;
253
+ }
254
+
255
+ e.preventDefault();
157
256
  e.stopImmediatePropagation();
158
- if (option.id === `${id}-selected` || !id) return;
159
257
 
160
- const currentlySelected = document.getElementById(`${id}-selected`);
258
+ const neighbor = (offset: number) => {
259
+ return optionElements.item((Array.from(optionElements).findIndex((x) => x.classList.contains("selected")) ?? -1) + offset)
260
+ }
261
+
262
+ if (e.key === "ArrowUp" && (focusIndex > 0 || !active)) {
263
+ if (!active) return neighbor(-1)?.click();
264
+ focusIndex--;
265
+ recomputeOptions();
266
+ }
161
267
 
268
+ if (e.key === "ArrowDown" && focusIndex + 1 < optionElements.length) {
269
+ if (!active) return neighbor(1)?.click();
270
+ focusIndex++;
271
+ recomputeOptions();
272
+ }
273
+
274
+ if (e.key === 'PageUp') {
275
+ focusIndex = 0;
276
+ if (!active) return optionElements.item(focusIndex)?.click();
277
+ recomputeOptions();
278
+ }
279
+ if (e.key === 'PageDown') {
280
+ focusIndex = optionElements.length - 1;
281
+ if (!active) return optionElements.item(focusIndex)?.click();
282
+ recomputeOptions();
283
+ }
284
+ });
285
+
286
+ const handleSelection = (e: MouseEvent, option: HTMLElement) => {
287
+ e.stopImmediatePropagation();
288
+ if (option.id === `${id}-selected` || !id) return;
289
+ const currentlySelected = document.getElementById(`${id}-selected`);
162
290
  if (currentlySelected) {
163
291
  currentlySelected.classList.remove("selected");
164
292
  currentlySelected.id = "";
165
293
  }
166
-
167
294
  option.id = `${id}-selected`;
168
295
  option.classList.add("selected");
169
-
170
- const opt = options[parseInt(option.dataset.optionIndex)];
296
+ const opt = options[parseInt(option.dataset.optionIndex!)];
171
297
  hiddenSelect.value = opt.value;
172
-
173
298
  valueSpan.textContent = opt.label;
174
- dropdown.classList.remove("active", "above");
175
-
176
- active = false;
299
+ closeDropdown();
177
300
  }
178
301
 
179
- option.addEventListener("click", handleSelection);
180
- option.addEventListener("keydown", (e) => {
181
- if (e.key === 'Enter') {
182
- handleSelection(e);
183
- }
184
- });
185
- });
302
+ optionElements.forEach((option) => {
303
+ const handleSelectionForOption = (e: MouseEvent) => handleSelection(e, option)
186
304
 
187
- window.addEventListener("scroll", () => {
188
- dropdown.classList.remove("active", "above");
189
- active = false;
190
- });
305
+ option.addEventListener("click", handleSelectionForOption);
306
+ });
191
307
 
192
- hideOnClickOutside(container);
308
+ window.addEventListener("scroll", closeDropdown);
193
309
 
194
- function hideOnClickOutside(element) {
195
- const outsideClickListener = (event) => {
196
- if (
197
- !element.contains(event.target) &&
198
- isVisible(element) &&
199
- active === true
200
- ) {
201
- // or use: event.target.closest(selector) === null
202
- dropdown.classList.remove("active", "above");
203
- active = false;
310
+ document.addEventListener("keydown", (e) => {
311
+ if (e.key === "Escape" && dropdown.classList.contains("active")) {
312
+ closeDropdown();
204
313
  }
205
- };
314
+ });
206
315
 
207
- const removeClickListener = () => {
208
- document.removeEventListener("click", outsideClickListener);
209
- };
316
+ hideOnClickOutside(container);
317
+
318
+ function hideOnClickOutside(element: HTMLElement) {
319
+ const outsideClickListener = (event: MouseEvent) => {
320
+ if (
321
+ !element.contains(event.target as HTMLElement) &&
322
+ isVisible(element) &&
323
+ active === true
324
+ ) {
325
+ // or use: event.target.closest(selector) === null
326
+ closeDropdown();
327
+ }
328
+ };
329
+
330
+ document.addEventListener("click", outsideClickListener);
331
+ }
210
332
 
211
- document.addEventListener("click", outsideClickListener);
333
+ // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
334
+ const isVisible = (elem: HTMLElement) =>
335
+ !!elem &&
336
+ !!(
337
+ elem.offsetWidth ||
338
+ elem.offsetHeight ||
339
+ elem.getClientRects().length
340
+ );
212
341
  }
213
-
214
- // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
215
- const isVisible = (elem) =>
216
- !!elem &&
217
- !!(
218
- elem.offsetWidth ||
219
- elem.offsetHeight ||
220
- elem.getClientRects().length
221
- );
222
342
  </script>
223
343
  <style>
224
344
  .sui-select-label {
@@ -256,7 +376,7 @@ const {
256
376
  border: 1px solid hsl(var(--border));
257
377
  background: hsl(var(--background-step-2));
258
378
  color: hsl(var(--text-normal));
259
- transition: all 0.15s ease;
379
+ transition: background border 0.15s ease;
260
380
  display: flex;
261
381
  flex-direction: row;
262
382
  align-items: center;
@@ -265,7 +385,11 @@ const {
265
385
  gap: 1rem;
266
386
  }
267
387
 
268
- .sui-select-button:hover {
388
+ .sui-select-button:focus {
389
+ border: 1px solid hsl(var(--primary-base));
390
+ }
391
+
392
+ .sui-select-button:hover, .sui-select-button:focus {
269
393
  background: hsl(var(--background-step-3));
270
394
  }
271
395
 
@@ -315,7 +439,7 @@ const {
315
439
  color: hsl(var(--text-muted));
316
440
  }
317
441
 
318
- .sui-select-option:hover, .sui-select-option:focus {
442
+ .sui-select-option:hover, .sui-select-option:focus, .sui-select-option.focused {
319
443
  background-color: hsl(var(--background-step-3));
320
444
  }
321
445
 
@@ -2,6 +2,10 @@ class SingleSidebarHelper {
2
2
  private sidebar: HTMLElement;
3
3
  private sidebarToggle?: HTMLElement | undefined;
4
4
 
5
+ /**
6
+ * A helper to manage the sidebar with.
7
+ * @param toggleID The ID of the element that should toggle the sidebar.
8
+ */
5
9
  constructor(toggleID?: string) {
6
10
  const sidebarContainer = document.getElementById('sui-sidebar');
7
11
 
@@ -28,6 +32,10 @@ class SingleSidebarHelper {
28
32
  }
29
33
  }
30
34
 
35
+ /**
36
+ * A helper function register an element which should toggle the sidebar.
37
+ * @param elementID The ID of the element that should toggle the sidebar.
38
+ */
31
39
  public toggleSidebarOnClick = (elementID: string) => {
32
40
  const navToggle = document.getElementById(elementID);
33
41
 
@@ -42,6 +50,10 @@ class SingleSidebarHelper {
42
50
  });
43
51
  };
44
52
 
53
+ /**
54
+ * A helper function to hide the sidebar when an element is clicked.
55
+ * @param elementID The ID of the element that should hide the sidebar.
56
+ */
45
57
  public hideSidebarOnClick = (elementID: string) => {
46
58
  const element = document.getElementById(elementID);
47
59
 
@@ -52,6 +64,10 @@ class SingleSidebarHelper {
52
64
  element.addEventListener('click', this.hideSidebar);
53
65
  };
54
66
 
67
+ /**
68
+ * A helper function to show the sidebar when an element is clicked.
69
+ * @param elementID The ID of the element that should show the sidebar.
70
+ */
55
71
  public showSidebarOnClick = (elementID: string) => {
56
72
  const element = document.getElementById(elementID);
57
73
 
@@ -62,10 +78,16 @@ class SingleSidebarHelper {
62
78
  element.addEventListener('click', this.showSidebar);
63
79
  };
64
80
 
81
+ /**
82
+ * A function to hide the sidebar.
83
+ */
65
84
  public hideSidebar = () => {
66
85
  this.sidebar.classList.remove('active');
67
86
  };
68
87
 
88
+ /**
89
+ * A function to show the sidebar.
90
+ */
69
91
  public showSidebar = () => {
70
92
  this.sidebar.classList.add('active');
71
93
  };
@@ -74,6 +96,9 @@ class SingleSidebarHelper {
74
96
  class DoubleSidebarHelper {
75
97
  private sidebarsContainer: HTMLElement;
76
98
 
99
+ /**
100
+ * A helper to manage the double sidebar with.
101
+ */
77
102
  constructor() {
78
103
  const sidebarsContainer = document.getElementById('sui-sidebars');
79
104
 
@@ -86,6 +111,10 @@ class DoubleSidebarHelper {
86
111
  this.sidebarsContainer = sidebarsContainer;
87
112
  }
88
113
 
114
+ /**
115
+ * A helper function to hide the sidebar when an element is clicked.
116
+ * @param elementID The ID of the element that should hide the sidebar.
117
+ */
89
118
  public hideSidebarOnClick = (elementID: string) => {
90
119
  const element = document.getElementById(elementID);
91
120
 
@@ -96,6 +125,10 @@ class DoubleSidebarHelper {
96
125
  element.addEventListener('click', this.hideSidebar);
97
126
  };
98
127
 
128
+ /**
129
+ * A helper function to show the outer sidebar when an element is clicked.
130
+ * @param elementID The ID of the element that should show the outer sidebar.
131
+ */
99
132
  public showOuterOnClick = (elementID: string) => {
100
133
  const element = document.getElementById(elementID);
101
134
 
@@ -106,6 +139,10 @@ class DoubleSidebarHelper {
106
139
  element.addEventListener('click', this.showOuterSidebar);
107
140
  };
108
141
 
142
+ /**
143
+ * A helper function to show the inner sidebar when an element is clicked.
144
+ * @param elementID The ID of the element that should show the inner sidebar.
145
+ */
109
146
  public showInnerOnClick = (elementID: string) => {
110
147
  const element = document.getElementById(elementID);
111
148
 
@@ -116,15 +153,24 @@ class DoubleSidebarHelper {
116
153
  element.addEventListener('click', this.showInnerSidebar);
117
154
  };
118
155
 
156
+ /**
157
+ * A function to show the inner sidebar.
158
+ */
119
159
  public showInnerSidebar = () => {
120
160
  this.sidebarsContainer.classList.add('inner', 'active');
121
161
  };
122
162
 
163
+ /**
164
+ * A function to show the outer sidebar.
165
+ */
123
166
  public showOuterSidebar = () => {
124
167
  this.sidebarsContainer.classList.add('active');
125
168
  this.sidebarsContainer.classList.remove('inner');
126
169
  };
127
170
 
171
+ /**
172
+ * A function to hide the sidebar altogether.
173
+ */
128
174
  public hideSidebar = () => {
129
175
  this.sidebarsContainer.classList.remove('inner', 'active');
130
176
  };
@@ -0,0 +1,47 @@
1
+ ---
2
+ import type { StudioCMSColorway } from '../../utils/colors';
3
+ import { generateID } from '../../utils/generateID';
4
+ import type { HeroIconName } from '../../utils/iconType';
5
+
6
+ /**
7
+ * The props for the TabItem component.
8
+ */
9
+ interface Props {
10
+ /**
11
+ * The icon to display next to the tab.
12
+ */
13
+ icon?: HeroIconName;
14
+ /**
15
+ * The label of the tab.
16
+ */
17
+ label: string;
18
+ /**
19
+ * The color of the tab. Defaults to `primary`.
20
+ */
21
+ color?: Exclude<StudioCMSColorway, 'default'>;
22
+ }
23
+
24
+ const id = generateID('tab');
25
+
26
+ const { icon, label, color = 'primary' } = Astro.props;
27
+ ---
28
+ <sui-tab-item
29
+ data-icon={icon}
30
+ data-label={label}
31
+ data-color={color}
32
+ data-tab-id={id}
33
+ class=""
34
+ >
35
+ <slot />
36
+ </sui-tab-item>
37
+ <style>
38
+ sui-tab-item {
39
+ display: none;
40
+ width: 100%;
41
+ height: auto;
42
+ }
43
+
44
+ sui-tab-item.active {
45
+ display: block;
46
+ }
47
+ </style>