@sveltia/ui 0.6.1 → 0.6.3

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.
@@ -7,6 +7,7 @@
7
7
  <svelte:options accessors={true} />
8
8
 
9
9
  <script>
10
+ import { activateKeyShortcuts } from '../util/events';
10
11
  import Popup from '../util/popup.svelte';
11
12
 
12
13
  /**
@@ -71,6 +72,12 @@
71
72
  */
72
73
  export let popupPosition = 'bottom-left';
73
74
 
75
+ /**
76
+ * Keyboard shortcuts.
77
+ * @type {string}
78
+ */
79
+ export let keyShortcuts = '';
80
+
74
81
  /** @type {?Popup} */
75
82
  let popupComponent;
76
83
  </script>
@@ -95,6 +102,7 @@
95
102
  on:keydown
96
103
  on:keyup
97
104
  on:keypress
105
+ use:activateKeyShortcuts={keyShortcuts}
98
106
  bind:this={element}
99
107
  >
100
108
  <slot name="start-icon" />
@@ -12,12 +12,13 @@ export default class Button extends SvelteComponent<{
12
12
  label?: string;
13
13
  type?: "reset" | "submit" | "button";
14
14
  disabled?: boolean;
15
+ size?: "small" | "medium" | "large";
15
16
  element?: HTMLButtonElement;
16
17
  role?: string;
17
- size?: "small" | "medium" | "large";
18
18
  hidden?: boolean;
19
19
  pressed?: boolean | "true" | "false" | "mixed";
20
20
  popupPosition?: PopupPosition;
21
+ keyShortcuts?: string;
21
22
  }, {
22
23
  mouseenter: MouseEvent;
23
24
  mouseleave: MouseEvent;
@@ -67,6 +68,9 @@ export default class Button extends SvelteComponent<{
67
68
  /**accessor*/
68
69
  set popupPosition(arg: PopupPosition);
69
70
  get popupPosition(): PopupPosition;
71
+ /**accessor*/
72
+ set keyShortcuts(arg: string);
73
+ get keyShortcuts(): string;
70
74
  }
71
75
  export type ButtonProps = typeof __propDef.props;
72
76
  export type ButtonEvents = typeof __propDef.events;
@@ -79,12 +83,13 @@ declare const __propDef: {
79
83
  label?: string;
80
84
  type?: ('button' | 'submit' | 'reset');
81
85
  disabled?: boolean;
86
+ size?: ('small' | 'medium' | 'large');
82
87
  element?: HTMLButtonElement | null;
83
88
  role?: string;
84
- size?: ('small' | 'medium' | 'large');
85
89
  hidden?: boolean;
86
90
  pressed?: (boolean | 'false' | 'mixed' | 'true' | undefined);
87
91
  popupPosition?: PopupPosition;
92
+ keyShortcuts?: string;
88
93
  };
89
94
  events: {
90
95
  mouseenter: MouseEvent;
@@ -12,10 +12,10 @@ export default class Checkbox extends SvelteComponent<{
12
12
  class?: string;
13
13
  name?: string;
14
14
  label?: string;
15
- disabled?: boolean;
16
- value?: string;
17
15
  checked?: string | boolean;
16
+ disabled?: boolean;
18
17
  indeterminate?: boolean;
18
+ value?: string;
19
19
  }, {
20
20
  change: CustomEvent<any>;
21
21
  } & {
@@ -34,10 +34,10 @@ declare const __propDef: {
34
34
  class?: string;
35
35
  name?: string;
36
36
  label?: string | null;
37
- disabled?: boolean;
38
- value?: string | null;
39
37
  checked?: (boolean | string | undefined);
38
+ disabled?: boolean;
40
39
  indeterminate?: boolean;
40
+ value?: string | null;
41
41
  };
42
42
  events: {
43
43
  change: CustomEvent<any>;
@@ -10,8 +10,8 @@ export default class Dialog extends SvelteComponent<{
10
10
  [x: string]: any;
11
11
  class?: string;
12
12
  title?: string;
13
- open?: boolean;
14
13
  size?: "small" | "medium" | "large" | "x-large";
14
+ open?: boolean;
15
15
  modal?: boolean;
16
16
  showCancel?: boolean;
17
17
  showOk?: boolean;
@@ -45,8 +45,8 @@ declare const __propDef: {
45
45
  [x: string]: any;
46
46
  class?: string;
47
47
  title?: string;
48
- open?: boolean;
49
48
  size?: ('small' | 'medium' | 'large' | 'x-large');
49
+ open?: boolean;
50
50
  modal?: boolean;
51
51
  showCancel?: boolean;
52
52
  showOk?: boolean;
@@ -11,8 +11,8 @@ export default class Drawer extends SvelteComponent<{
11
11
  class?: string;
12
12
  title?: string;
13
13
  position?: "top" | "right" | "bottom" | "left";
14
- open?: boolean;
15
14
  size?: "small" | "medium" | "large" | "x-large";
15
+ open?: boolean;
16
16
  showClose?: false | "inside" | "outside";
17
17
  closeOnBackdropClick?: boolean;
18
18
  }, {
@@ -39,8 +39,8 @@ declare const __propDef: {
39
39
  class?: string;
40
40
  title?: string;
41
41
  position?: ('top' | 'right' | 'bottom' | 'left');
42
- open?: boolean;
43
42
  size?: ('small' | 'medium' | 'large' | 'x-large');
43
+ open?: boolean;
44
44
  showClose?: ('inside' | 'outside' | false);
45
45
  closeOnBackdropClick?: boolean;
46
46
  };
@@ -10,8 +10,8 @@
10
10
  export default class Listbox extends SvelteComponent<{
11
11
  [x: string]: any;
12
12
  class?: string;
13
- element?: HTMLElement;
14
13
  multiple?: boolean;
14
+ element?: HTMLElement;
15
15
  }, {
16
16
  click: MouseEvent;
17
17
  select: Event;
@@ -38,8 +38,8 @@ declare const __propDef: {
38
38
  props: {
39
39
  [x: string]: any;
40
40
  class?: string;
41
- element?: HTMLElement | null;
42
41
  multiple?: boolean;
42
+ element?: HTMLElement | null;
43
43
  };
44
44
  events: {
45
45
  click: MouseEvent;
@@ -9,8 +9,8 @@ export default class MenuItem extends SvelteComponent<{
9
9
  [x: string]: any;
10
10
  class?: string;
11
11
  label?: string;
12
- role?: "menuitem" | "menuitemcheckbox" | "menuitemradio";
13
12
  checked?: boolean;
13
+ role?: "menuitem" | "menuitemcheckbox" | "menuitemradio";
14
14
  iconName?: string;
15
15
  iconLabel?: string;
16
16
  }, {
@@ -30,8 +30,8 @@ declare const __propDef: {
30
30
  [x: string]: any;
31
31
  class?: string;
32
32
  label?: string;
33
- role?: ('menuitem' | 'menuitemcheckbox' | 'menuitemradio');
34
33
  checked?: boolean;
34
+ role?: ('menuitem' | 'menuitemcheckbox' | 'menuitemradio');
35
35
  iconName?: string;
36
36
  iconLabel?: string;
37
37
  };
@@ -12,8 +12,8 @@ export default class Combobox extends SvelteComponent<{
12
12
  label?: string;
13
13
  position?: PopupPosition;
14
14
  disabled?: boolean;
15
- value?: string | number;
16
15
  readOnly?: boolean;
16
+ value?: string | number;
17
17
  }, {
18
18
  change: CustomEvent<any>;
19
19
  } & {
@@ -36,8 +36,8 @@ declare const __propDef: {
36
36
  label?: string;
37
37
  position?: PopupPosition;
38
38
  disabled?: boolean;
39
- value?: (string | number | undefined);
40
39
  readOnly?: boolean;
40
+ value?: (string | number | undefined);
41
41
  };
42
42
  events: {
43
43
  change: CustomEvent<any>;
@@ -12,13 +12,13 @@
12
12
  export default class Slider extends SvelteComponent<{
13
13
  [x: string]: any;
14
14
  class?: string;
15
+ max?: number;
16
+ min?: number;
17
+ step?: number;
15
18
  value?: number;
16
19
  sliderLabel?: string;
17
20
  values?: [number, number];
18
21
  sliderLabels?: [string, string];
19
- min?: number;
20
- max?: number;
21
- step?: number;
22
22
  optionLabels?: string[] | number[];
23
23
  }, {
24
24
  click: MouseEvent;
@@ -35,13 +35,13 @@ declare const __propDef: {
35
35
  props: {
36
36
  [x: string]: any;
37
37
  class?: string;
38
+ max?: number;
39
+ min?: number;
40
+ step?: number;
38
41
  value?: number;
39
42
  sliderLabel?: string;
40
43
  values?: [number, number];
41
44
  sliderLabels?: [string, string];
42
- min?: number;
43
- max?: number;
44
- step?: number;
45
45
  optionLabels?: (string[] | number[]);
46
46
  };
47
47
  events: {
@@ -10,8 +10,8 @@ export default class Switch extends SvelteComponent<{
10
10
  [x: string]: any;
11
11
  class?: string;
12
12
  label?: string;
13
- disabled?: boolean;
14
13
  checked?: boolean;
14
+ disabled?: boolean;
15
15
  }, {
16
16
  [evt: string]: CustomEvent<any>;
17
17
  }, {
@@ -27,8 +27,8 @@ declare const __propDef: {
27
27
  [x: string]: any;
28
28
  class?: string;
29
29
  label?: string;
30
- disabled?: boolean;
31
30
  checked?: boolean;
31
+ disabled?: boolean;
32
32
  };
33
33
  events: {
34
34
  [evt: string]: CustomEvent<any>;
@@ -10,10 +10,10 @@ export default class NumberInput extends SvelteComponent<{
10
10
  [x: string]: any;
11
11
  class?: string;
12
12
  disabled?: boolean;
13
- value?: string;
14
- min?: any;
15
13
  max?: any;
14
+ min?: any;
16
15
  step?: number;
16
+ value?: string;
17
17
  }, {
18
18
  keypress: KeyboardEvent;
19
19
  input: Event;
@@ -30,10 +30,10 @@ declare const __propDef: {
30
30
  [x: string]: any;
31
31
  class?: string;
32
32
  disabled?: boolean;
33
- value?: string | null;
34
- min?: any;
35
33
  max?: any;
34
+ min?: any;
36
35
  step?: number;
36
+ value?: string | null;
37
37
  };
38
38
  events: {
39
39
  keypress: KeyboardEvent;
@@ -1,14 +1,13 @@
1
1
  <!--
2
2
  @component
3
- A multi-line text field. The equivalent of the HTML `<textarea>` element.
3
+ A multi-line text field based on the HTML `<textarea>` element, providing the auto-resize support.
4
4
  @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea
5
5
  @see https://w3c.github.io/aria/#textbox
6
+ @see https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/
6
7
  -->
7
8
  <svelte:options accessors={true} />
8
9
 
9
10
  <script>
10
- import { onMount, tick } from 'svelte';
11
-
12
11
  /**
13
12
  * CSS class name on the button.
14
13
  * @type {string}
@@ -26,80 +25,32 @@
26
25
  export let value = undefined;
27
26
 
28
27
  export let autoResize = false;
29
-
30
- /** @type {string?} */
31
- let height;
32
- /** @type {HTMLElement?} */
33
- let outer = undefined;
34
- let resizing = false;
35
- let lastWidth = 0;
36
-
37
- /**
38
- * Resize the `<textarea>` based on the filled text content.
39
- */
40
- const resizeTextarea = async () => {
41
- resizing = true;
42
- height = 'auto';
43
-
44
- await tick();
45
-
46
- height = value && element?.scrollHeight ? `${element.scrollHeight + 4}px` : undefined;
47
- resizing = false;
48
- };
49
-
50
- /**
51
- * Call {@link resizeTextarea} whenever the text content is updated.
52
- */
53
- $: {
54
- if (autoResize) {
55
- // @ts-ignore
56
- resizeTextarea(value);
57
- }
58
- }
59
-
60
- /**
61
- * Call {@link resizeTextarea} whenever it’s horizontally resized.
62
- */
63
- onMount(() => {
64
- const observer = new ResizeObserver(([{ contentRect }]) => {
65
- const { width } = contentRect;
66
-
67
- if (autoResize && lastWidth !== width) {
68
- lastWidth = width;
69
- resizeTextarea();
70
- }
71
- });
72
-
73
- observer.observe(outer);
74
-
75
- // onUnmount
76
- return () => {
77
- observer.disconnect();
78
- };
79
- });
80
28
  </script>
81
29
 
82
- <div class="sui text-area {className}" bind:this={outer}>
30
+ <div class="sui text-area {className}">
83
31
  <textarea
84
32
  name={name || undefined}
85
33
  {...$$restProps}
86
- style:height
87
- class:resizing
34
+ class:auto-resize={autoResize}
88
35
  bind:this={element}
89
36
  bind:value
90
37
  on:click
91
38
  on:input
92
39
  on:keypress
93
40
  />
41
+ {#if autoResize}
42
+ <div class="clone" aria-hidden="true">{value}</div>
43
+ {/if}
94
44
  </div>
95
45
 
96
46
  <style>.text-area {
47
+ display: inline-grid;
97
48
  width: 100%;
98
- display: inline-flex;
99
- align-items: center;
100
49
  }
101
50
 
102
- textarea {
51
+ textarea,
52
+ .clone {
53
+ grid-area: 1/1/2/2;
103
54
  display: block;
104
55
  margin: 0;
105
56
  border-width: 1px;
@@ -113,17 +64,32 @@ textarea {
113
64
  font-family: var(--sui-textbox-font-family);
114
65
  font-size: var(--sui-textbox-font-size);
115
66
  line-height: var(--sui-textbox-multiline-line-height);
116
- resize: vertical;
117
67
  transition: all 200ms;
118
68
  }
119
- textarea.resizing {
69
+ textarea.resizing,
70
+ .clone.resizing {
120
71
  transition-duration: 0ms;
121
72
  }
122
- textarea:focus {
73
+ textarea:focus,
74
+ .clone:focus {
123
75
  border-color: var(--sui-primary-accent-color);
124
76
  }
125
- textarea:disabled {
77
+ textarea:disabled,
78
+ .clone:disabled {
126
79
  background-color: var(--sui-disabled-background-color);
127
80
  opacity: 0.4;
128
81
  cursor: default;
82
+ }
83
+
84
+ textarea {
85
+ resize: vertical;
86
+ }
87
+ textarea.auto-resize {
88
+ overflow: hidden;
89
+ resize: none;
90
+ }
91
+
92
+ .clone {
93
+ visibility: hidden;
94
+ white-space: pre-wrap;
129
95
  }</style>
@@ -2,9 +2,10 @@
2
2
  /** @typedef {typeof __propDef.events} TextAreaEvents */
3
3
  /** @typedef {typeof __propDef.slots} TextAreaSlots */
4
4
  /**
5
- * A multi-line text field. The equivalent of the HTML `<textarea>` element.
5
+ * A multi-line text field based on the HTML `<textarea>` element, providing the auto-resize support.
6
6
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea
7
7
  * @see https://w3c.github.io/aria/#textbox
8
+ * @see https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/
8
9
  */
9
10
  export default class TextArea extends SvelteComponent<{
10
11
  [x: string]: any;
@@ -7,6 +7,7 @@
7
7
  <svelte:options accessors={true} />
8
8
 
9
9
  <script>
10
+ import { activateKeyShortcuts } from '../util/events';
10
11
  import { getRandomId } from '../util/util';
11
12
 
12
13
  /**
@@ -35,6 +36,12 @@
35
36
  /** @type {(string | number | undefined)} */
36
37
  export let value = undefined;
37
38
 
39
+ /**
40
+ * Keyboard shortcuts.
41
+ * @type {string}
42
+ */
43
+ export let keyShortcuts = '';
44
+
38
45
  const id = getRandomId('input');
39
46
  let ariaLabel = '';
40
47
 
@@ -66,6 +73,7 @@
66
73
  on:keyup
67
74
  on:keypress
68
75
  on:change
76
+ use:activateKeyShortcuts={keyShortcuts}
69
77
  />
70
78
  {#if ariaLabel}
71
79
  <span id="{id}-label" class="label" class:hidden={!!value}>
@@ -11,10 +11,11 @@ export default class TextInput extends SvelteComponent<{
11
11
  class?: string;
12
12
  name?: string;
13
13
  disabled?: boolean;
14
+ readOnly?: boolean;
14
15
  value?: string | number;
15
16
  element?: HTMLInputElement;
16
17
  role?: "textbox" | "searchbox" | "combobox" | "spinbutton";
17
- readOnly?: boolean;
18
+ keyShortcuts?: string;
18
19
  }, {
19
20
  input: Event;
20
21
  keydown: KeyboardEvent;
@@ -45,6 +46,9 @@ export default class TextInput extends SvelteComponent<{
45
46
  /**accessor*/
46
47
  set value(arg: string | number);
47
48
  get value(): string | number;
49
+ /**accessor*/
50
+ set keyShortcuts(arg: string);
51
+ get keyShortcuts(): string;
48
52
  }
49
53
  export type TextInputProps = typeof __propDef.props;
50
54
  export type TextInputEvents = typeof __propDef.events;
@@ -56,10 +60,11 @@ declare const __propDef: {
56
60
  class?: string;
57
61
  name?: string;
58
62
  disabled?: boolean;
63
+ readOnly?: boolean;
59
64
  value?: (string | number | undefined);
60
65
  element?: HTMLInputElement | null;
61
66
  role?: ('textbox' | 'searchbox' | 'combobox' | 'spinbutton');
62
- readOnly?: boolean;
67
+ keyShortcuts?: string;
63
68
  };
64
69
  events: {
65
70
  input: Event;
@@ -0,0 +1,3 @@
1
+ export function isMac(): boolean;
2
+ export function matchShortcuts(event: KeyboardEvent, shortcuts: string): boolean;
3
+ export function activateKeyShortcuts(element: (HTMLInputElement | HTMLButtonElement), shortcuts: string): import('svelte/action').ActionReturn;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Check if the user agent is macOS.
3
+ * @returns {boolean} Result.
4
+ */
5
+ export const isMac = () =>
6
+ /** @type {any} */ (navigator).userAgentData?.platform === 'macOS' ||
7
+ navigator.platform.startsWith('Mac');
8
+
9
+ /**
10
+ * Whether the event matches the given keyboard shortcuts.
11
+ * @param {KeyboardEvent} event `keydown` or `keypress` event.
12
+ * @param {string} shortcuts Keyboard shortcuts like `A` or `Ctrl+S`.
13
+ * @returns {boolean} Result.
14
+ * @see https://w3c.github.io/aria/#aria-keyshortcuts
15
+ */
16
+ export const matchShortcuts = (event, shortcuts) => {
17
+ const { ctrlKey, metaKey, altKey, shiftKey, key } = event;
18
+
19
+ return shortcuts.split(/\s+/).some((shortcut) => {
20
+ const keys = shortcut.split('+');
21
+
22
+ // Check if required modifier keys are pressed
23
+ if (
24
+ (keys.includes('Ctrl') && !ctrlKey) ||
25
+ (keys.includes('Meta') && !metaKey) ||
26
+ (keys.includes('Alt') && !altKey) ||
27
+ (keys.includes('Shift') && !shiftKey)
28
+ ) {
29
+ return false;
30
+ }
31
+
32
+ // Check if unnecessary modifier keys are not pressed
33
+ if (
34
+ (!keys.includes('Ctrl') && ctrlKey) ||
35
+ (!keys.includes('Meta') && metaKey) ||
36
+ (!keys.includes('Alt') && altKey) ||
37
+ (!keys.includes('Shift') && shiftKey)
38
+ ) {
39
+ return false;
40
+ }
41
+
42
+ return keys
43
+ .filter((_key) => !['Ctrl', 'Meta', 'Alt', 'Shift'].includes(_key))
44
+ .every((_key) => _key.toUpperCase() === key.toUpperCase());
45
+ });
46
+ };
47
+
48
+ /**
49
+ * Activate keyboard shortcuts.
50
+ * @param {(HTMLInputElement | HTMLButtonElement)} element Element.
51
+ * @param {string} shortcuts Keyboard shortcuts like `A` or `Accel+S` to focus and click the text
52
+ * field or button. Multiple shortcuts can be defined space-separated. The `Accel` modifier will be
53
+ * replaced with `Ctrl` on Windows/Linux and `Command` on macOS.
54
+ * @returns {import('svelte/action').ActionReturn} Actions.
55
+ */
56
+ export const activateKeyShortcuts = (element, shortcuts) => {
57
+ let platformKeyShortcuts;
58
+
59
+ /**
60
+ * Handle the event.
61
+ * @param {KeyboardEvent} event `keydown` event.
62
+ */
63
+ const handler = (event) => {
64
+ if (!!element.offsetParent && matchShortcuts(event, platformKeyShortcuts)) {
65
+ event.preventDefault();
66
+
67
+ if (!element.disabled) {
68
+ element.focus();
69
+ element.click();
70
+ }
71
+ }
72
+ };
73
+
74
+ /**
75
+ * Remove the event listener.
76
+ */
77
+ const removeListener = () => {
78
+ window.removeEventListener('keydown', handler);
79
+ element.removeAttribute('aria-keyshortcuts');
80
+ };
81
+
82
+ /**
83
+ * Add the event listener.
84
+ */
85
+ const addListener = () => {
86
+ platformKeyShortcuts = shortcuts
87
+ ? shortcuts.replace(/\bAccel\b/g, isMac() ? 'Meta' : 'Ctrl')
88
+ : undefined;
89
+
90
+ if (platformKeyShortcuts) {
91
+ window.addEventListener('keydown', handler);
92
+ element.setAttribute('aria-keyshortcuts', platformKeyShortcuts);
93
+ }
94
+ };
95
+
96
+ addListener();
97
+
98
+ return {
99
+ // eslint-disable-next-line jsdoc/require-jsdoc, no-shadow, no-unused-vars
100
+ update(shortcuts) {
101
+ removeListener();
102
+ addListener();
103
+ },
104
+
105
+ // eslint-disable-next-line jsdoc/require-jsdoc
106
+ destroy() {
107
+ removeListener();
108
+ },
109
+ };
110
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -291,6 +291,10 @@
291
291
  "svelte": "./package/components/util/app-shell.svelte",
292
292
  "default": "./package/components/util/app-shell.svelte"
293
293
  },
294
+ "./components/util/events": {
295
+ "types": "./package/components/util/events.d.ts",
296
+ "default": "./package/components/util/events.js"
297
+ },
294
298
  "./components/util/group": {
295
299
  "types": "./package/components/util/group.d.ts",
296
300
  "default": "./package/components/util/group.js"
@@ -485,6 +489,9 @@
485
489
  "components/util/app-shell.svelte": [
486
490
  "./package/components/util/app-shell.svelte.d.ts"
487
491
  ],
492
+ "components/util/events": [
493
+ "./package/components/util/events.d.ts"
494
+ ],
488
495
  "components/util/group": [
489
496
  "./package/components/util/group.d.ts"
490
497
  ],