customized carbon components
mellbacon committed
Aug 9, 2022 at 18:09 UTC
c752c19f29fed34da97c95c23a8aaeb2a3913cdb
2 files changed
+451
src/components/ContextMenu/ContextMenu.svelte
new
+210
@@ -0,0 +1,210 @@
1
+<script>
2
+ /**
3
+ * @event {HTMLElement} open
4
+ */
5
+
6
+ /**
7
+ * Specify an element or list of elements to trigger the context menu.
8
+ * If no element is specified, the context menu applies to the entire window
9
+ * @type {null | ReadonlyArray<null | HTMLElement>}
10
+ */
11
+ export let target = null;
12
+
13
+ /**
14
+ * Set to `true` to open the menu
15
+ * Either `x` and `y` must be greater than zero
16
+ */
17
+ export let open = false;
18
+
19
+ /** Specify the horizontal offset of the menu position */
20
+ export let x = 0;
21
+
22
+ /** Specify the vertical offset of the menu position */
23
+ export let y = 0;
24
+
25
+ /** Obtain a reference to the unordered list HTML element */
26
+ export let ref = null;
27
+
28
+ import {
29
+ onMount,
30
+ setContext,
31
+ getContext,
32
+ afterUpdate,
33
+ createEventDispatcher,
34
+ } from "svelte";
35
+ import { writable } from "svelte/store";
36
+
37
+ const dispatch = createEventDispatcher();
38
+ const position = writable([x, y]);
39
+ const currentIndex = writable(-1);
40
+ const hasPopup = writable(false);
41
+ const menuOffsetX = writable(0);
42
+ const ctx = getContext("ContextMenu");
43
+
44
+ let options = [];
45
+ let direction = 1;
46
+ let prevX = 0;
47
+ let prevY = 0;
48
+ let focusIndex = -1;
49
+ let openDetail = null;
50
+
51
+ function close() {
52
+ let menus = document.querySelectorAll(".bx--menu");
53
+ for (let i = 0; i < menus.length; i++) {
54
+ menus[i].classList.remove("bx--menu--open");
55
+ }
56
+ open = false;
57
+ x = 0;
58
+ y = 0;
59
+ prevX = 0;
60
+ prevY = 0;
61
+ focusIndex = -1;
62
+ }
63
+
64
+ /** @type {(e: MouseEvent) => void} */
65
+ function openMenu(e) {
66
+ close();
67
+ const { height, width } = ref.getBoundingClientRect();
68
+
69
+ if (open || x === 0) {
70
+ if (window.innerWidth - width < e.x) {
71
+ x = e.x - width;
72
+ } else {
73
+ x = e.x;
74
+ }
75
+ }
76
+
77
+ if (open || y === 0) {
78
+ menuOffsetX.set(e.x);
79
+
80
+ if (window.innerHeight - height < e.y) {
81
+ y = e.y - height;
82
+ } else {
83
+ y = e.y;
84
+ }
85
+ }
86
+ position.set([x, y]);
87
+ open = true;
88
+ openDetail = e.target;
89
+ }
90
+
91
+ $: if (target != null) {
92
+ if (Array.isArray(target)) {
93
+ target.forEach((node) =>
94
+ node?.addEventListener("contextmenu", openMenu)
95
+ );
96
+ } else {
97
+ target.addEventListener("contextmenu", openMenu);
98
+ }
99
+ }
100
+
101
+ onMount(() => {
102
+ return () => {
103
+ if (target != null) {
104
+ if (Array.isArray(target)) {
105
+ target.forEach((node) =>
106
+ node?.removeEventListener("contextmenu", openMenu)
107
+ );
108
+ } else {
109
+ target.removeEventListener("contextmenu", openMenu);
110
+ }
111
+ }
112
+ };
113
+ });
114
+
115
+ setContext("ContextMenu", {
116
+ menuOffsetX,
117
+ currentIndex,
118
+ position,
119
+ close,
120
+ setPopup: (popup) => {
121
+ hasPopup.set(popup);
122
+ },
123
+ });
124
+
125
+ afterUpdate(() => {
126
+ if (open) {
127
+ options = [...ref.querySelectorAll("li[data-nested='false']")];
128
+
129
+ if (level === 1) {
130
+ if (prevX !== x || prevY !== y) ref.focus();
131
+ prevX = x;
132
+ prevY = y;
133
+ }
134
+
135
+ dispatch("open", openDetail);
136
+ } else {
137
+ dispatch("close");
138
+ }
139
+
140
+ if (!$hasPopup && options[focusIndex]) options[focusIndex].focus();
141
+ });
142
+
143
+ $: level = !ctx ? 1 : 2;
144
+ $: currentIndex.set(focusIndex);
145
+</script>
146
+
147
+<svelte:window
148
+ on:contextmenu|preventDefault={(e) => {
149
+ if (target != null) return;
150
+ if (level > 1) return;
151
+ if (!ref) return;
152
+ openMenu(e);
153
+ }}
154
+ on:click={(e) => {
155
+ if (!open) return;
156
+ close();
157
+ }}
158
+ on:keydown={(e) => {
159
+ if (open && e.key === "Escape") close();
160
+ }}
161
+/>
162
+
163
+<ul
164
+ bind:this={ref}
165
+ role="menu"
166
+ tabindex="-1"
167
+ data-direction={direction}
168
+ data-level={level}
169
+ class:bx--menu={true}
170
+ class:bx--menu--open={open}
171
+ class:bx--menu--invisible={open && x === 0 && y === 0}
172
+ class:bx--menu--root={level === 1}
173
+ {...$$restProps}
174
+ style="left: {x}px; top: {y}px; {$$restProps.style}"
175
+ on:click
176
+ on:click={({ target }) => {
177
+ const closestOption = target.closest("[tabindex]");
178
+ if (
179
+ closestOption &&
180
+ closestOption.getAttribute("role") !== "menuitem"
181
+ ) {
182
+ close();
183
+ }
184
+ }}
185
+ on:keydown
186
+ on:keydown={(e) => {
187
+ if (open) e.preventDefault();
188
+ if ($hasPopup) return;
189
+ if (e.key === "ArrowDown") {
190
+ if (focusIndex < options.length - 1) focusIndex++;
191
+ } else if (e.key === "ArrowUp") {
192
+ if (focusIndex === -1) {
193
+ focusIndex = options.length - 1;
194
+ } else {
195
+ if (focusIndex > 0) focusIndex--;
196
+ }
197
+ }
198
+ }}
199
+>
200
+ <slot />
201
+</ul>
202
+
203
+<style>
204
+ :global(.bx--menu:focus) {
205
+ outline: none;
206
+ }
207
+ :global(.bx--menu-option:focus) {
208
+ outline: none;
209
+ }
210
+</style>
\ No newline at end of file
src/components/Input/Input.svelte
new
+241
@@ -0,0 +1,241 @@
1
+<script>
2
+ /**
3
+ * @event {null | number | string} change
4
+ * @event {null | number | string} input
5
+ */
6
+
7
+ /**
8
+ * Set the size of the input
9
+ * @type {"sm" | "xl"}
10
+ */
11
+ export let size = undefined;
12
+
13
+ /**
14
+ * Specify the input value.
15
+ *
16
+ * `value` will be set to `null` if type="number"
17
+ * and the value is empty.
18
+ * @type {null | number | string}
19
+ */
20
+ export let value = "";
21
+
22
+ /** Specify the placeholder text */
23
+ export let placeholder = "";
24
+
25
+ /** Set to `true` to enable the light variant */
26
+ export let light = false;
27
+
28
+ /** Set to `true` to disable the input */
29
+ export let disabled = false;
30
+
31
+ /** Specify the helper text */
32
+ export let helperText = "";
33
+
34
+ /** Set an id for the input element */
35
+ export let id = "ccs-" + Math.random().toString(36);
36
+ export let _class = "";
37
+
38
+ /**
39
+ * Specify a name attribute for the input
40
+ * @type {string}
41
+ */
42
+ export let name = undefined;
43
+
44
+ /** Specify the label text */
45
+ export let labelText = "";
46
+
47
+ /** Set to `true` to visually hide the label text */
48
+ export let hideLabel = false;
49
+
50
+ /** Set to `true` to indicate an invalid state */
51
+ export let invalid = false;
52
+
53
+ /** Specify the invalid state text */
54
+ export let invalidText = "";
55
+
56
+ /** Set to `true` to indicate an warning state */
57
+ export let warn = false;
58
+
59
+ /** Specify the warning state text */
60
+ export let warnText = "";
61
+
62
+ /** Obtain a reference to the input HTML element */
63
+ export let ref = null;
64
+
65
+ /** Set to `true` to mark the field as required */
66
+ export let required = false;
67
+
68
+ /** Set to `true` to use the inline variant */
69
+ export let inline = false;
70
+
71
+ /** Set to `true` to use the read-only variant */
72
+ export let readonly = false;
73
+
74
+ import { createEventDispatcher, getContext } from "svelte";
75
+ import WarningFilled from "carbon-icons-svelte/lib/WarningFilled.svelte";
76
+ import WarningAltFilled from "carbon-icons-svelte/lib/WarningAltFilled.svelte";
77
+ import EditOff from "carbon-icons-svelte/lib/EditOff.svelte";
78
+
79
+ const ctx = getContext("Form");
80
+ const dispatch = createEventDispatcher();
81
+
82
+ function parse(raw) {
83
+ if ($$restProps.type !== "number") return raw;
84
+ return raw != "" ? Number(raw) : null;
85
+ }
86
+
87
+ /** @type {(e: Event) => void} */
88
+ const onInput = (e) => {
89
+ value = parse(e.target.value);
90
+ dispatch("input", value);
91
+ };
92
+
93
+ /** @type {(e: Event) => void} */
94
+ const onChange = (e) => {
95
+ dispatch("change", parse(e.target.value));
96
+ };
97
+
98
+ $: isFluid = !!ctx && ctx.isFluid;
99
+ $: errorId = `error-${id}`;
100
+ $: warnId = `warn-${id}`;
101
+</script>
102
+
103
+<!-- svelte-ignore a11y-mouse-events-have-key-events -->
104
+<div
105
+ class:bx--form-item={true}
106
+ class:bx--text-input-wrapper={true}
107
+ class:bx--text-input-wrapper--inline={inline}
108
+ class:bx--text-input-wrapper--light={light}
109
+ class:bx--text-input-wrapper--readonly={readonly}
110
+ class={_class}
111
+ on:click
112
+ on:mouseover
113
+ on:mouseenter
114
+ on:mouseleave
115
+>
116
+ {#if inline}
117
+ <div class:bx--text-input__label-helper-wrapper={true}>
118
+ {#if labelText}
119
+ <label
120
+ for={id}
121
+ class:bx--label={true}
122
+ class:bx--visually-hidden={hideLabel}
123
+ class:bx--label--disabled={disabled}
124
+ class:bx--label--inline={inline}
125
+ class:bx--label--inline--sm={size === "sm"}
126
+ class:bx--label--inline--xl={size === "xl"}
127
+ >
128
+ <slot name="labelText">
129
+ {labelText}
130
+ </slot>
131
+ </label>
132
+ {/if}
133
+ {#if !isFluid && helperText}
134
+ <div
135
+ class:bx--form__helper-text={true}
136
+ class:bx--form__helper-text--disabled={disabled}
137
+ class:bx--form__helper-text--inline={inline}
138
+ >
139
+ {helperText}
140
+ </div>
141
+ {/if}
142
+ </div>
143
+ {/if}
144
+ {#if !inline && (labelText || $$slots.labelText)}
145
+ <label
146
+ for={id}
147
+ class:bx--label={true}
148
+ class:bx--visually-hidden={hideLabel}
149
+ class:bx--label--disabled={disabled}
150
+ class:bx--label--inline={inline}
151
+ class:bx--label--inline-sm={inline && size === "sm"}
152
+ class:bx--label--inline-xl={inline && size === "xl"}
153
+ >
154
+ <slot name="labelText">
155
+ {labelText}
156
+ </slot>
157
+ </label>
158
+ {/if}
159
+ <div
160
+ class:bx--text-input__field-outer-wrapper={true}
161
+ class:bx--text-input__field-outer-wrapper--inline={inline}
162
+ >
163
+ <div
164
+ data-invalid={invalid || undefined}
165
+ data-warn={warn || undefined}
166
+ class:bx--text-input__field-wrapper={true}
167
+ class:bx--text-input__field-wrapper--warning={!invalid && warn}
168
+ >
169
+ {#if invalid}
170
+ <WarningFilled class="bx--text-input__invalid-icon" />
171
+ {/if}
172
+ {#if !invalid && warn}
173
+ <WarningAltFilled
174
+ class="bx--text-input__invalid-icon
175
+ bx--text-input__invalid-icon--warning"
176
+ />
177
+ {/if}
178
+ {#if readonly}
179
+ <EditOff class="bx--text-input__readonly-icon" />
180
+ {/if}
181
+ <input
182
+ bind:this={ref}
183
+ data-invalid={invalid || undefined}
184
+ aria-invalid={invalid || undefined}
185
+ data-warn={warn || undefined}
186
+ aria-describedby={invalid ? errorId : warn ? warnId : undefined}
187
+ {disabled}
188
+ {id}
189
+ {name}
190
+ {placeholder}
191
+ bind:value
192
+ {required}
193
+ {readonly}
194
+ class:bx--text-input={true}
195
+ class:bx--text-input--light={light}
196
+ class:bx--text-input--invalid={invalid}
197
+ class:bx--text-input--warn={warn}
198
+ class:bx--text-input--sm={size === "sm"}
199
+ class:bx--text-input--xl={size === "xl"}
200
+ {...$$restProps}
201
+ on:change={onChange}
202
+ on:input={onInput}
203
+ on:keydown
204
+ on:keyup
205
+ on:focus
206
+ on:blur
207
+ on:paste
208
+ />
209
+ {#if isFluid}
210
+ <hr class:bx--text-input__divider={true} />
211
+ {/if}
212
+ {#if isFluid && !inline && invalid}
213
+ <div class:bx--form-requirement={true} id={errorId}>
214
+ {invalidText}
215
+ </div>
216
+ {/if}
217
+ {#if isFluid && !inline && warn}
218
+ <div class:bx--form-requirement={true} id={warnId}>
219
+ {warnText}
220
+ </div>
221
+ {/if}
222
+ </div>
223
+ {#if !invalid && !warn && !isFluid && !inline && helperText}
224
+ <div
225
+ class:bx--form__helper-text={true}
226
+ class:bx--form__helper-text--disabled={disabled}
227
+ class:bx--form__helper-text--inline={inline}
228
+ >
229
+ {helperText}
230
+ </div>
231
+ {/if}
232
+ {#if !isFluid && invalid}
233
+ <div class:bx--form-requirement={true} id={errorId}>
234
+ {invalidText}
235
+ </div>
236
+ {/if}
237
+ {#if !isFluid && !invalid && warn}
238
+ <div class:bx--form-requirement={true} id={warnId}>{warnText}</div>
239
+ {/if}
240
+ </div>
241
+</div>