@setoelkahfi / svara / commits / c752c19

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>