@sveltia/ui 0.13.1 → 0.14.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.
@@ -20,6 +20,10 @@
20
20
  * @type {boolean}
21
21
  */
22
22
  export let multiple = false;
23
+ /**
24
+ * Whether to select a row by clicking on it.
25
+ */
26
+ export let clickToSelect = true;
23
27
 
24
28
  /** @type {HTMLElement | undefined} */
25
29
  export let element = undefined;
@@ -33,7 +37,7 @@
33
37
  aria-multiselectable={multiple}
34
38
  {...$$restProps}
35
39
  bind:this={element}
36
- use:activateGroup
40
+ use:activateGroup={{ clickToSelect }}
37
41
  on:change={(/** @type {CustomEvent} */ event) => {
38
42
  dispatch('change', event.detail);
39
43
  }}
@@ -10,6 +10,7 @@ export default class Grid extends SvelteComponent<{
10
10
  [x: string]: any;
11
11
  class?: string | undefined;
12
12
  element?: HTMLElement | undefined;
13
+ clickToSelect?: boolean | undefined;
13
14
  multiple?: boolean | undefined;
14
15
  }, {
15
16
  change: CustomEvent<any>;
@@ -28,6 +29,7 @@ declare const __propDef: {
28
29
  [x: string]: any;
29
30
  class?: string | undefined;
30
31
  element?: HTMLElement | undefined;
32
+ clickToSelect?: boolean | undefined;
31
33
  multiple?: boolean | undefined;
32
34
  };
33
35
  events: {
@@ -87,7 +87,6 @@
87
87
  */
88
88
  const onChange = () => {
89
89
  const selected = popupComponent?.content?.querySelector(selectedSelector);
90
-
91
90
  const target = /** @type {HTMLButtonElement} */ (
92
91
  popupComponent?.content?.querySelector(`[role="option"][value="${value}"]`)
93
92
  );
@@ -87,7 +87,6 @@
87
87
 
88
88
  const fromIndex = positionList.findLastIndex((s) => s <= diff);
89
89
  const toIndex = positionList.findIndex((s) => diff <= s);
90
-
91
90
  const index =
92
91
  Math.abs(positionList[fromIndex] - diff) < Math.abs(positionList[toIndex] - diff)
93
92
  ? fromIndex
@@ -150,6 +150,7 @@ export const initEditor = () => {
150
150
  TOGGLE_LINK_COMMAND,
151
151
  (payload) => {
152
152
  toggleLink(typeof payload === 'string' ? payload : null);
153
+
153
154
  return true;
154
155
  },
155
156
  COMMAND_PRIORITY_NORMAL,
@@ -159,6 +160,7 @@ export const initEditor = () => {
159
160
  INSERT_UNORDERED_LIST_COMMAND,
160
161
  () => {
161
162
  insertList(editor, 'bullet');
163
+
162
164
  return true;
163
165
  },
164
166
  COMMAND_PRIORITY_NORMAL,
@@ -168,6 +170,7 @@ export const initEditor = () => {
168
170
  INSERT_ORDERED_LIST_COMMAND,
169
171
  () => {
170
172
  insertList(editor, 'number');
173
+
171
174
  return true;
172
175
  },
173
176
  COMMAND_PRIORITY_NORMAL,
@@ -74,17 +74,14 @@ export const availableButtons = {
74
74
  inline: false,
75
75
  },
76
76
  };
77
-
78
77
  /**
79
78
  * @type {TextEditorFormatType[]}
80
79
  */
81
80
  export const textFormatButtonTypes = ['bold', 'italic', 'code'];
82
-
83
81
  /**
84
82
  * @type {TextEditorInlineType[]}
85
83
  */
86
84
  export const inlineButtonTypes = [...textFormatButtonTypes, 'link'];
87
-
88
85
  /**
89
86
  * @type {TextEditorBlockType[]}
90
87
  */
@@ -4,7 +4,7 @@
4
4
  -->
5
5
  <script>
6
6
  import { generateElementId } from '@sveltia/utils/element';
7
- import { onMount, setContext } from 'svelte';
7
+ import { setContext } from 'svelte';
8
8
  import { _ } from 'svelte-i18n';
9
9
  import { writable } from 'svelte/store';
10
10
  import Alert from '../alert/alert.svelte';
@@ -70,26 +70,65 @@
70
70
  const editorId = writable(generateElementId('editor'));
71
71
  const useRichText = writable(modes[0] === 'rich-text');
72
72
  const hasConverterError = writable(false);
73
+ let inputValue = '';
73
74
  let showConverterError = false;
74
75
 
75
76
  /**
76
- * Convert the Markdown {@link value} to Lexical nodes. Disable the rich text mode and restore the
77
- * original value when there is an error while conversion.
77
+ * Convert the Markdown {@link inputValue} to Lexical nodes. Disable the rich text mode and
78
+ * restore the original value when there is an error while conversion.
78
79
  */
79
80
  const convertMarkdown = async () => {
80
- const originalValue = value;
81
+ const originalValue = inputValue;
81
82
 
82
83
  try {
83
84
  // We should avoid an empty editor; there should be at least one `<p>`, so give it an empty
84
85
  // string if the `value` is `undefined`
85
86
  // @see https://github.com/facebook/lexical/issues/2308
86
- await convertMarkdownToLexical($editor, value ?? '');
87
+ await convertMarkdownToLexical($editor, inputValue ?? '');
87
88
  } catch {
88
89
  $hasConverterError = true;
89
- value = originalValue;
90
+ inputValue = originalValue;
90
91
  }
91
92
  };
92
93
 
94
+ /**
95
+ * Update {@link inputValue} based on {@link value}.
96
+ */
97
+ const setInputValue = () => {
98
+ const newValue = value ?? '';
99
+
100
+ // Avoid a cycle dependency & infinite loop
101
+ if (inputValue !== newValue) {
102
+ inputValue = newValue;
103
+
104
+ if ($useRichText) {
105
+ convertMarkdown();
106
+ }
107
+ }
108
+ };
109
+
110
+ /**
111
+ * Update {@link value} based on {@link inputValue}.
112
+ */
113
+ const setCurrentValue = () => {
114
+ const newValue = inputValue;
115
+
116
+ // Avoid a cycle dependency & infinite loop
117
+ if (value !== newValue) {
118
+ value = newValue;
119
+ }
120
+ };
121
+
122
+ $: {
123
+ void value;
124
+ setInputValue();
125
+ }
126
+
127
+ $: {
128
+ void inputValue;
129
+ setCurrentValue();
130
+ }
131
+
93
132
  $: {
94
133
  if ($hasConverterError) {
95
134
  $useRichText = false;
@@ -111,18 +150,12 @@
111
150
  convertMarkdown,
112
151
  }),
113
152
  );
114
-
115
- onMount(() => {
116
- if ($useRichText) {
117
- convertMarkdown();
118
- }
119
- });
120
153
  </script>
121
154
 
122
155
  <div role="none" class="sui text-editor" hidden={hidden || undefined} {...$$restProps}>
123
156
  <EditorToolbar {disabled} {readonly} />
124
157
  <LexicalRoot
125
- bind:value
158
+ bind:value={inputValue}
126
159
  hidden={!$useRichText || hidden}
127
160
  {disabled}
128
161
  {readonly}
@@ -131,7 +164,7 @@
131
164
  />
132
165
  <TextArea
133
166
  autoResize={true}
134
- bind:value
167
+ bind:value={inputValue}
135
168
  {flex}
136
169
  hidden={$useRichText || hidden}
137
170
  {disabled}
@@ -121,6 +121,12 @@ textarea,
121
121
  font-family: var(--sui-textbox-font-family);
122
122
  font-size: var(--sui-textbox-font-size);
123
123
  line-height: var(--sui-textbox-multiline-line-height);
124
+ font-weight: var(--sui-textbox-font-weight, normal);
125
+ text-align: var(--sui-textbox-text-align, left);
126
+ text-indent: var(--sui-textbox-text-indent, 0);
127
+ text-transform: var(--sui-textbox-text-transform, none);
128
+ letter-spacing: var(--sui-textbox-letter-spacing, normal);
129
+ word-spacing: var(--sui-word-spacing-normal, normal);
124
130
  transition: all 200ms;
125
131
  }
126
132
  textarea.resizing,
@@ -166,6 +166,7 @@ input {
166
166
  text-indent: var(--sui-textbox-text-indent, 0);
167
167
  text-transform: var(--sui-textbox-text-transform, none);
168
168
  letter-spacing: var(--sui-textbox-letter-spacing, normal);
169
+ word-spacing: var(--sui-word-spacing-normal, normal);
169
170
  transition: all 200ms;
170
171
  }
171
172
  input:focus {
@@ -228,6 +228,9 @@ dialog:not(.open) {
228
228
  transition-duration: 400ms;
229
229
  opacity: 0;
230
230
  }
231
+ dialog[hidden] {
232
+ transition-duration: 1ms !important;
233
+ }
231
234
  dialog:not(.active) {
232
235
  pointer-events: none !important;
233
236
  }
@@ -1 +1 @@
1
- export function activateGroup(parent: HTMLElement, _params?: object | undefined): import('svelte/action').ActionReturn;
1
+ export function activateGroup(parent: HTMLElement, params?: object | undefined): import('svelte/action').ActionReturn;
@@ -62,9 +62,11 @@ class Group {
62
62
  /**
63
63
  * Initialize a new `Group` instance.
64
64
  * @param {HTMLElement} parent - Parent element.
65
+ * @param {object} [options] - Options.
66
+ * @param {boolean} [options.clickToSelect] - Whether to select an item by clicking on it.
65
67
  * @todo Check for added elements probably with `MutationObserver`.
66
68
  */
67
- constructor(parent) {
69
+ constructor(parent, { clickToSelect = true } = {}) {
68
70
  parent.dispatchEvent(new CustomEvent('initializing'));
69
71
 
70
72
  this.parent = parent;
@@ -72,6 +74,7 @@ class Group {
72
74
  this.multi = this.parent.getAttribute('aria-multiselectable') === 'true';
73
75
  this.id = generateElementId(this.role);
74
76
  this.parentGroupSelector = `[role="group"], [role="${this.role}"]`;
77
+ this.clickToSelect = clickToSelect;
75
78
 
76
79
  const { orientation, childRoles, childSelectedAttr, focusChild, selectFirst } =
77
80
  config[this.role];
@@ -103,7 +106,6 @@ class Group {
103
106
  const isSelected = defaultSelected
104
107
  ? element === defaultSelected
105
108
  : this.selectFirst && index === 0;
106
-
107
109
  const controlTarget = /** @type {HTMLElement | null} */ (
108
110
  document.querySelector(`#${element.getAttribute('aria-controls')}`)
109
111
  );
@@ -219,7 +221,6 @@ class Group {
219
221
  const targetRole = newTarget.getAttribute('role');
220
222
  const targetParent = newTarget.closest(this.parentGroupSelector);
221
223
  const selectByClick = event.type === 'click';
222
-
223
224
  const selectByKeydown =
224
225
  event.type === 'keydown' && /** @type {KeyboardEvent} */ (event).key === ' ';
225
226
 
@@ -335,12 +336,11 @@ class Group {
335
336
  onClick(event) {
336
337
  // eslint-disable-next-line prefer-destructuring
337
338
  const target = /** @type {HTMLElement} */ (event.target);
338
-
339
339
  const newTarget = target.matches(this.selector)
340
340
  ? target
341
341
  : /** @type {HTMLElement | null} */ (target.closest(this.selector));
342
342
 
343
- if (!newTarget || event.button !== 0) {
343
+ if (!newTarget || event.button !== 0 || !this.clickToSelect) {
344
344
  return;
345
345
  }
346
346
 
@@ -362,7 +362,6 @@ class Group {
362
362
  // eslint-disable-next-line prefer-destructuring
363
363
  const target = /** @type {HTMLElement} */ (event.target);
364
364
  const { allMembers, activeMembers } = this;
365
-
366
365
  /** @type {HTMLElement | undefined} */
367
366
  const currentTarget = (() => {
368
367
  if (!this.focusChild) {
@@ -461,7 +460,6 @@ class Group {
461
460
  onUpdate({ searchTerms }) {
462
461
  const terms = searchTerms.trim().toLocaleLowerCase();
463
462
  const _terms = terms ? terms.split(/\s+/) : [];
464
-
465
463
  const matched = this.allMembers
466
464
  .map((member) => {
467
465
  const searchValue =
@@ -471,7 +469,6 @@ class Group {
471
469
  member.querySelector('.label')?.textContent ??
472
470
  member.textContent
473
471
  )?.toLocaleLowerCase() ?? '';
474
-
475
472
  const hidden = !_terms.every((term) => searchValue.includes(term));
476
473
 
477
474
  member.dispatchEvent(new CustomEvent('toggle', { detail: { hidden } }));
@@ -489,20 +486,20 @@ class Group {
489
486
  /**
490
487
  * Activate a new group.
491
488
  * @param {HTMLElement} parent - Parent element.
492
- * @param {object} [_params] - Action params.
489
+ * @param {object} [params] - Action params.
493
490
  * @returns {import('svelte/action').ActionReturn} Action.
494
491
  */
495
492
  // eslint-disable-next-line no-unused-vars
496
- export const activateGroup = (parent, _params) => {
497
- const group = new Group(parent);
493
+ export const activateGroup = (parent, params) => {
494
+ const group = new Group(parent, params);
498
495
 
499
496
  return {
500
497
  /**
501
498
  * Called whenever the params are updated.
502
- * @param {any} params - Updated params.
499
+ * @param {any} newParams - Updated params.
503
500
  */
504
- update(params) {
505
- group.onUpdate(params);
501
+ update(newParams) {
502
+ group.onUpdate(newParams);
506
503
  },
507
504
  };
508
505
  };
@@ -48,5 +48,9 @@ declare class Popup {
48
48
  * Check the position of the anchor element.
49
49
  */
50
50
  checkPosition(): void;
51
+ /**
52
+ * Hide the popup immediately (when the anchor is being hidden).
53
+ */
54
+ hideImmediately(): Promise<void>;
51
55
  }
52
56
  export {};
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
 
3
3
  import { generateElementId } from '@sveltia/utils/element';
4
+ import { sleep } from '@sveltia/utils/misc';
4
5
  import { get, writable } from 'svelte/store';
5
6
 
6
7
  /**
@@ -27,7 +28,6 @@ class Popup {
27
28
 
28
29
  const { scrollHeight: contentHeight, scrollWidth: contentWidth } =
29
30
  /** @type {HTMLElement} */ (this.popupElement.querySelector('.content'));
30
-
31
31
  const topMargin = intersectionRect.top - 8;
32
32
  const bottomMargin = rootBounds.height - intersectionRect.bottom - 8;
33
33
  let { position } = this;
@@ -58,27 +58,22 @@ class Popup {
58
58
  : position.endsWith('-top')
59
59
  ? `${Math.round(intersectionRect.top)}px`
60
60
  : 'auto';
61
-
62
61
  const right = position.startsWith('left-')
63
62
  ? `${Math.round(rootBounds.width - intersectionRect.left)}px`
64
63
  : position.endsWith('-right')
65
64
  ? `${Math.round(rootBounds.width - intersectionRect.right)}px`
66
65
  : 'auto';
67
-
68
66
  const bottom = position.startsWith('top-')
69
67
  ? `${Math.round(rootBounds.height - intersectionRect.top)}px`
70
68
  : position.endsWith('-bottom')
71
69
  ? `${Math.round(rootBounds.height - intersectionRect.bottom)}px`
72
70
  : 'auto';
73
-
74
71
  const left = position.startsWith('right-')
75
72
  ? `${Math.round(intersectionRect.right)}px`
76
73
  : position.endsWith('-left')
77
74
  ? `${Math.round(intersectionRect.left)}px`
78
75
  : 'auto';
79
-
80
76
  const anchorPopup = /** @type {HTMLElement} */ (this.anchorElement.closest('.popup'));
81
-
82
77
  const style = {
83
78
  inset: [top, right, bottom, left].join(' '),
84
79
  zIndex: anchorPopup ? Number(anchorPopup.style.zIndex) + 1 : 1000,
@@ -127,6 +122,18 @@ class Popup {
127
122
  }
128
123
  });
129
124
 
125
+ this.anchorElement.addEventListener('transitionstart', () => {
126
+ if (this.anchorElement.closest('.hiding, .hidden, [hidden]')) {
127
+ this.hideImmediately();
128
+ }
129
+ });
130
+
131
+ new IntersectionObserver(([entry]) => {
132
+ if (!entry.isIntersecting && get(this.open)) {
133
+ this.hideImmediately();
134
+ }
135
+ }).observe(this.anchorElement);
136
+
130
137
  // Close the popup when the backdrop, a menu item or an option is clicked
131
138
  this.popupElement.addEventListener('click', (event) => {
132
139
  event.stopPropagation();
@@ -188,6 +195,16 @@ class Popup {
188
195
  this.observer.unobserve(this.positionBaseElement);
189
196
  this.observer.observe(this.positionBaseElement);
190
197
  }
198
+
199
+ /**
200
+ * Hide the popup immediately (when the anchor is being hidden).
201
+ */
202
+ async hideImmediately() {
203
+ this.popupElement.hidden = true;
204
+ this.open.set(false);
205
+ await sleep(50);
206
+ this.popupElement.hidden = false;
207
+ }
191
208
  }
192
209
 
193
210
  /**
@@ -44,7 +44,7 @@ type TextEditorState = {
44
44
  */
45
45
  enabledButtons: (TextEditorBlockType | TextEditorInlineType)[];
46
46
  /**
47
- * Function to trigger the Lexical converter.
47
+ * - Function to trigger the Lexical converter.
48
48
  */
49
49
  convertMarkdown: Function;
50
50
  };
@@ -41,5 +41,5 @@
41
41
  * error while converting Markdown to Lexical nodes.
42
42
  * @property {(TextEditorBlockType | TextEditorInlineType)[]} enabledButtons - Enabled buttons for
43
43
  * the editor.
44
- * @property {Function} convertMarkdown Function to trigger the Lexical converter.
44
+ * @property {Function} convertMarkdown - Function to trigger the Lexical converter.
45
45
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -23,47 +23,47 @@
23
23
  "test:unit": "vitest"
24
24
  },
25
25
  "dependencies": {
26
- "@lexical/code": "^0.14.5",
27
- "@lexical/history": "^0.14.5",
28
- "@lexical/link": "^0.14.5",
29
- "@lexical/list": "^0.14.5",
30
- "@lexical/markdown": "^0.14.5",
31
- "@lexical/rich-text": "^0.14.5",
32
- "@lexical/selection": "^0.14.5",
33
- "@lexical/table": "^0.14.5",
34
- "@lexical/utils": "^0.14.5",
35
- "@sveltia/utils": "^0.1.1",
36
- "lexical": "^0.14.5",
37
- "svelte": "^4.2.15"
26
+ "@lexical/code": "^0.15.0",
27
+ "@lexical/history": "^0.15.0",
28
+ "@lexical/link": "^0.15.0",
29
+ "@lexical/list": "^0.15.0",
30
+ "@lexical/markdown": "^0.15.0",
31
+ "@lexical/rich-text": "^0.15.0",
32
+ "@lexical/selection": "^0.15.0",
33
+ "@lexical/table": "^0.15.0",
34
+ "@lexical/utils": "^0.15.0",
35
+ "@sveltia/utils": "^0.2.0",
36
+ "lexical": "^0.15.0",
37
+ "svelte": "^4.2.16"
38
38
  },
39
39
  "devDependencies": {
40
- "@playwright/test": "^1.43.1",
40
+ "@playwright/test": "^1.44.0",
41
41
  "@sveltejs/adapter-auto": "^3.2.0",
42
42
  "@sveltejs/kit": "^2.5.7",
43
43
  "@sveltejs/package": "^2.3.1",
44
44
  "@sveltejs/vite-plugin-svelte": "^3.1.0",
45
- "cspell": "^8.7.0",
45
+ "cspell": "^8.8.1",
46
46
  "eslint": "^8.57.0",
47
47
  "eslint-config-airbnb-base": "^15.0.0",
48
48
  "eslint-config-prettier": "^9.1.0",
49
49
  "eslint-plugin-import": "^2.29.1",
50
- "eslint-plugin-jsdoc": "^48.2.3",
51
- "eslint-plugin-svelte": "^2.38.0",
50
+ "eslint-plugin-jsdoc": "^48.2.4",
51
+ "eslint-plugin-svelte": "^2.39.0",
52
52
  "npm-run-all": "^4.1.5",
53
53
  "postcss": "^8.4.38",
54
- "postcss-html": "^1.6.0",
54
+ "postcss-html": "^1.7.0",
55
55
  "prettier": "^3.2.5",
56
56
  "prettier-plugin-svelte": "^3.2.3",
57
- "sass": "^1.75.0",
58
- "stylelint": "^16.4.0",
57
+ "sass": "^1.77.1",
58
+ "stylelint": "^16.5.0",
59
59
  "stylelint-config-recommended-scss": "^14.0.0",
60
- "stylelint-scss": "^6.2.1",
61
- "svelte-check": "^3.7.0",
60
+ "stylelint-scss": "^6.3.0",
61
+ "svelte-check": "^3.7.1",
62
62
  "svelte-i18n": "^4.0.0",
63
63
  "svelte-preprocess": "^5.1.4",
64
64
  "tslib": "^2.6.2",
65
- "vite": "^5.2.10",
66
- "vitest": "^1.5.2"
65
+ "vite": "^5.2.11",
66
+ "vitest": "^1.6.0"
67
67
  },
68
68
  "exports": {
69
69
  "./package.json": "./package.json",