@sveltia/ui 0.71.1 → 0.72.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.
- package/dist/components/alert/infobar.svelte +1 -1
- package/dist/components/button/button.svelte +1 -1
- package/dist/components/button/select-button.svelte +2 -1
- package/dist/components/calendar/calendar.d.ts +20 -0
- package/dist/components/calendar/calendar.js +81 -0
- package/dist/components/calendar/calendar.svelte +23 -45
- package/dist/components/checkbox/checkbox-group.svelte +1 -1
- package/dist/components/checkbox/checkbox.svelte +6 -1
- package/dist/components/dialog/dialog.svelte +1 -1
- package/dist/components/drawer/drawer.svelte +1 -1
- package/dist/components/emoji/emoji-suggestions.svelte +14 -40
- package/dist/components/emoji/position.d.ts +44 -0
- package/dist/components/emoji/position.js +60 -0
- package/dist/components/progressbar/progressbar.svelte +1 -1
- package/dist/components/radio/radio-group.svelte +1 -1
- package/dist/components/resizable-pane/resizable-handle.svelte +15 -47
- package/dist/components/resizable-pane/resizable-pane-group.svelte +37 -100
- package/dist/components/resizable-pane/resizable-pane.svelte +1 -1
- package/dist/components/resizable-pane/sizing.d.ts +48 -0
- package/dist/components/resizable-pane/sizing.js +190 -0
- package/dist/components/scroll/infinite-scroll.svelte +2 -0
- package/dist/components/select/combobox.svelte +3 -1
- package/dist/components/select/select-tags.d.ts +17 -0
- package/dist/components/select/select-tags.js +95 -0
- package/dist/components/select/select-tags.svelte +26 -28
- package/dist/components/slider/slider.d.ts +17 -0
- package/dist/components/slider/slider.js +96 -0
- package/dist/components/slider/slider.svelte +30 -56
- package/dist/components/tabs/tab-box.svelte +1 -1
- package/dist/components/tabs/tab-list.d.ts +6 -0
- package/dist/components/tabs/tab-list.js +19 -0
- package/dist/components/tabs/tab-list.svelte +2 -14
- package/dist/components/text-editor/code-editor.d.ts +5 -0
- package/dist/components/text-editor/code-editor.js +28 -0
- package/dist/components/text-editor/code-editor.svelte +7 -8
- package/dist/components/text-editor/emoji-autocomplete.svelte +9 -1
- package/dist/components/text-editor/lexical-root.svelte +3 -1
- package/dist/components/text-editor/shiki/facade.js +1 -1
- package/dist/components/text-editor/shiki/highlighter.js +40 -15
- package/dist/components/text-editor/shiki/version.d.ts +1 -1
- package/dist/components/text-editor/shiki/version.js +1 -1
- package/dist/components/text-editor/store.svelte.js +17 -7
- package/dist/components/text-editor/text-editor.svelte +4 -0
- package/dist/components/text-editor/toolbar/code-editor-toolbar.svelte +1 -1
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte +4 -1
- package/dist/components/text-editor/toolbar/format-text-button.svelte +3 -1
- package/dist/components/text-editor/toolbar/insert-item-button.svelte +1 -1
- package/dist/components/text-editor/toolbar/insert-link-button.svelte +10 -3
- package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte +4 -5
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +2 -0
- package/dist/components/text-field/emoji-autocomplete.svelte +2 -0
- package/dist/components/text-field/number-input.d.ts +15 -0
- package/dist/components/text-field/number-input.js +98 -0
- package/dist/components/text-field/number-input.svelte +27 -23
- package/dist/components/text-field/text-input.svelte +9 -3
- package/dist/components/toast/toast.svelte +7 -2
- package/dist/components/toolbar/toolbar.svelte +1 -1
- package/dist/components/typography/truncated-text.svelte +2 -1
- package/dist/components/util/app-shell.d.ts +1 -0
- package/dist/components/util/app-shell.js +29 -0
- package/dist/components/util/app-shell.svelte +4 -18
- package/dist/components/util/modal.d.ts +5 -0
- package/dist/components/util/modal.js +22 -0
- package/dist/components/util/modal.svelte +9 -10
- package/dist/components/util/popup.svelte +11 -0
- package/dist/locales/ar.yaml +2 -2
- package/dist/locales/ca.yaml +2 -2
- package/dist/locales/cs.yaml +2 -2
- package/dist/locales/da.yaml +2 -2
- package/dist/locales/de.yaml +1 -1
- package/dist/locales/el.yaml +2 -2
- package/dist/locales/en-CA.yaml +1 -1
- package/dist/locales/en-GB.yaml +1 -1
- package/dist/locales/en-US.yaml +1 -1
- package/dist/locales/es-CO.yaml +2 -2
- package/dist/locales/fa.yaml +2 -2
- package/dist/locales/fi.yaml +1 -1
- package/dist/locales/fr.yaml +2 -2
- package/dist/locales/hr.yaml +116 -0
- package/dist/locales/it.yaml +2 -2
- package/dist/locales/nl.yaml +1 -1
- package/dist/locales/pl.yaml +2 -2
- package/dist/locales/pt-BR.yaml +2 -2
- package/dist/locales/pt-PT.yaml +2 -2
- package/dist/locales/sv.yaml +2 -2
- package/dist/locales/zh-CN.yaml +1 -1
- package/dist/locales/zh-TW.yaml +1 -1
- package/package.json +8 -4
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
class="sui select-button {className}"
|
|
37
37
|
aria-checked={selected}
|
|
38
38
|
onChange={(event) => {
|
|
39
|
-
|
|
39
|
+
// The radio group reports the state as `checked`, after the `aria-checked` attribute it manages
|
|
40
|
+
selected = event.detail.checked;
|
|
40
41
|
onChange?.(event);
|
|
41
42
|
}}
|
|
42
43
|
></Button>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date helpers behind `<Calendar>`, kept apart from the component so the grid can be tested without
|
|
3
|
+
* rendering anything.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Number of cells in the day grid: six weeks, enough to show any month in full from the Sunday on
|
|
7
|
+
* or before its first day.
|
|
8
|
+
*/
|
|
9
|
+
export const DAY_GRID_SIZE: 42;
|
|
10
|
+
/**
|
|
11
|
+
* List of month names for the month selector. We use a fixed date to avoid issues with daylight
|
|
12
|
+
* saving time.
|
|
13
|
+
* @type {string[]}
|
|
14
|
+
*/
|
|
15
|
+
export const MONTH_NAMES: string[];
|
|
16
|
+
export function getFirstDayOfMonth(date: Date): Date;
|
|
17
|
+
export function addMonths(firstDay: Date, delta: number): Date;
|
|
18
|
+
export function getCalendarDays(firstDay: Date): Date[];
|
|
19
|
+
export function toDateString(date: Date): string;
|
|
20
|
+
export function isSameDay(a: Date, b: Date): boolean;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date helpers behind `<Calendar>`, kept apart from the component so the grid can be tested without
|
|
3
|
+
* rendering anything.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Number of cells in the day grid: six weeks, enough to show any month in full from the Sunday on
|
|
8
|
+
* or before its first day.
|
|
9
|
+
*/
|
|
10
|
+
export const DAY_GRID_SIZE = 42;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* List of month names for the month selector. We use a fixed date to avoid issues with daylight
|
|
14
|
+
* saving time.
|
|
15
|
+
* @type {string[]}
|
|
16
|
+
*/
|
|
17
|
+
export const MONTH_NAMES = Array.from({ length: 12 }, (__, i) =>
|
|
18
|
+
new Date(2000, i, 10).toLocaleDateString('en', { month: 'short' }),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the first day of the month the given date is in.
|
|
23
|
+
* @param {Date} date Any date within the month.
|
|
24
|
+
* @returns {Date} Local midnight on the first day of the month, using the UTC year and month of
|
|
25
|
+
* the given date, so a date parsed from a `YYYY-MM-DD` string lands in the right month regardless
|
|
26
|
+
* of the time zone.
|
|
27
|
+
*/
|
|
28
|
+
export const getFirstDayOfMonth = (date) => new Date(date.getUTCFullYear(), date.getUTCMonth(), 1);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the first day of another month, relative to the given one.
|
|
32
|
+
* @param {Date} firstDay First day of a month.
|
|
33
|
+
* @param {number} delta Number of months to move by; negative to go back.
|
|
34
|
+
* @returns {Date} A new date, `delta` months away.
|
|
35
|
+
*/
|
|
36
|
+
export const addMonths = (firstDay, delta) => {
|
|
37
|
+
const date = new Date(firstDay);
|
|
38
|
+
|
|
39
|
+
date.setUTCMonth(date.getUTCMonth() + delta);
|
|
40
|
+
|
|
41
|
+
return date;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the days to be laid out in the calendar grid.
|
|
46
|
+
* @param {Date} firstDay First day of the month being displayed.
|
|
47
|
+
* @returns {Date[]} {@link DAY_GRID_SIZE} consecutive days, starting from the Sunday on or before
|
|
48
|
+
* `firstDay`, so the days from the previous and next months fill the grid.
|
|
49
|
+
*/
|
|
50
|
+
export const getCalendarDays = (firstDay) => {
|
|
51
|
+
const cursor = new Date(firstDay);
|
|
52
|
+
|
|
53
|
+
// Start from Sunday
|
|
54
|
+
cursor.setDate(1 - cursor.getUTCDay());
|
|
55
|
+
|
|
56
|
+
return Array.from({ length: DAY_GRID_SIZE }, () => {
|
|
57
|
+
const day = new Date(cursor);
|
|
58
|
+
|
|
59
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
60
|
+
|
|
61
|
+
return day;
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format a date as the `value` of the calendar.
|
|
67
|
+
* @param {Date} date Date.
|
|
68
|
+
* @returns {string} Date in the `YYYY-MM-DD` format, based on UTC.
|
|
69
|
+
*/
|
|
70
|
+
export const toDateString = (date) => date.toJSON().split('T')[0];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether two dates fall on the same local calendar day.
|
|
74
|
+
* @param {Date} a A date.
|
|
75
|
+
* @param {Date} b Another date.
|
|
76
|
+
* @returns {boolean} Result.
|
|
77
|
+
*/
|
|
78
|
+
export const isSameDay = (a, b) =>
|
|
79
|
+
a.getFullYear() === b.getFullYear() &&
|
|
80
|
+
a.getMonth() === b.getMonth() &&
|
|
81
|
+
a.getDate() === b.getDate();
|
|
@@ -4,25 +4,24 @@
|
|
|
4
4
|
-->
|
|
5
5
|
<script>
|
|
6
6
|
import { _, isRTL } from '@sveltia/i18n';
|
|
7
|
-
import { SvelteDate } from 'svelte/reactivity';
|
|
8
7
|
import Button from '../button/button.svelte';
|
|
9
8
|
import Divider from '../divider/divider.svelte';
|
|
10
9
|
import Spacer from '../divider/spacer.svelte';
|
|
11
10
|
import Icon from '../icon/icon.svelte';
|
|
11
|
+
import {
|
|
12
|
+
addMonths,
|
|
13
|
+
getCalendarDays,
|
|
14
|
+
getFirstDayOfMonth,
|
|
15
|
+
isSameDay,
|
|
16
|
+
MONTH_NAMES,
|
|
17
|
+
toDateString,
|
|
18
|
+
} from './calendar.js';
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* @typedef {object} Props
|
|
15
22
|
* @property {string} [value] Date.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
|
-
/**
|
|
19
|
-
* List of month names for month selector. We use a fixed date to avoid issues with daylight
|
|
20
|
-
* saving time.
|
|
21
|
-
*/
|
|
22
|
-
const MONTH_NAMES = Array.from({ length: 12 }, (__, i) =>
|
|
23
|
-
new Date(2000, i, 10).toLocaleDateString('en', { month: 'short' }),
|
|
24
|
-
);
|
|
25
|
-
|
|
26
25
|
/**
|
|
27
26
|
* @type {Props & Record<string, any>}
|
|
28
27
|
*/
|
|
@@ -31,30 +30,13 @@
|
|
|
31
30
|
const now = new Date();
|
|
32
31
|
|
|
33
32
|
const date = $derived(value ? new Date(value) : now);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const dayList = $derived.by(() => {
|
|
43
|
-
if (!firstDay) return [];
|
|
44
|
-
|
|
45
|
-
const cursor = new SvelteDate(firstDay);
|
|
46
|
-
|
|
47
|
-
// Start from Sunday
|
|
48
|
-
cursor.setDate(1 - cursor.getUTCDay());
|
|
49
|
-
|
|
50
|
-
return Array.from({ length: 42 }, () => {
|
|
51
|
-
const day = new Date(cursor);
|
|
52
|
-
|
|
53
|
-
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
54
|
-
|
|
55
|
-
return { day };
|
|
56
|
-
});
|
|
57
|
-
});
|
|
33
|
+
/**
|
|
34
|
+
* First day of the month being displayed. Follows the {@link value}, and is reassigned by the
|
|
35
|
+
* month navigation below.
|
|
36
|
+
* @type {Date}
|
|
37
|
+
*/
|
|
38
|
+
let firstDay = $derived(getFirstDayOfMonth(date));
|
|
39
|
+
const dayList = $derived(getCalendarDays(firstDay).map((day) => ({ day })));
|
|
58
40
|
</script>
|
|
59
41
|
|
|
60
42
|
<div role="group">
|
|
@@ -62,7 +44,7 @@
|
|
|
62
44
|
<div role="none" class="header">
|
|
63
45
|
<Button
|
|
64
46
|
variant="ghost"
|
|
65
|
-
label={firstDay
|
|
47
|
+
label={firstDay.toLocaleDateString('en', { year: 'numeric', month: 'short' })}
|
|
66
48
|
aria-haspopup="dialog"
|
|
67
49
|
>
|
|
68
50
|
{#snippet endIcon()}
|
|
@@ -98,7 +80,7 @@
|
|
|
98
80
|
<div role="none" class="grid">
|
|
99
81
|
{#each [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as year (year)}
|
|
100
82
|
<div role="none">
|
|
101
|
-
<Button>202{year}</Button>
|
|
83
|
+
<Button>{`202${year}`}</Button>
|
|
102
84
|
</div>
|
|
103
85
|
{/each}
|
|
104
86
|
</div>
|
|
@@ -119,8 +101,7 @@
|
|
|
119
101
|
<Button
|
|
120
102
|
aria-label={_('_sui.calendar.previous_month')}
|
|
121
103
|
onclick={() => {
|
|
122
|
-
firstDay
|
|
123
|
-
firstDay = firstDay;
|
|
104
|
+
firstDay = addMonths(firstDay, -1);
|
|
124
105
|
}}
|
|
125
106
|
>
|
|
126
107
|
<Icon name="chevron_left" />
|
|
@@ -128,8 +109,7 @@
|
|
|
128
109
|
<Button
|
|
129
110
|
aria-label={_('_sui.calendar.next_month')}
|
|
130
111
|
onclick={() => {
|
|
131
|
-
firstDay
|
|
132
|
-
firstDay = firstDay;
|
|
112
|
+
firstDay = addMonths(firstDay, 1);
|
|
133
113
|
}}
|
|
134
114
|
>
|
|
135
115
|
<Icon name={isRTL() ? 'chevron_left' : 'chevron_right'} />
|
|
@@ -144,16 +124,14 @@
|
|
|
144
124
|
{#each dayList as { day } (day)}
|
|
145
125
|
<div
|
|
146
126
|
role="none"
|
|
147
|
-
class:other-month={day.getUTCMonth() !== firstDay
|
|
148
|
-
class:today={day
|
|
149
|
-
day.getMonth() === now.getMonth() &&
|
|
150
|
-
day.getDate() === now.getDate()}
|
|
127
|
+
class:other-month={day.getUTCMonth() !== firstDay.getUTCMonth()}
|
|
128
|
+
class:today={isSameDay(day, now)}
|
|
151
129
|
>
|
|
152
130
|
<Button
|
|
153
131
|
role="option"
|
|
154
132
|
aria-selected={false}
|
|
155
133
|
onclick={() => {
|
|
156
|
-
|
|
134
|
+
value = toDateString(day);
|
|
157
135
|
}}
|
|
158
136
|
>
|
|
159
137
|
{day.getUTCDate()}
|
|
@@ -172,7 +150,7 @@
|
|
|
172
150
|
<Spacer flex={true} />
|
|
173
151
|
<Button
|
|
174
152
|
onclick={() => {
|
|
175
|
-
|
|
153
|
+
value = toDateString(now);
|
|
176
154
|
}}
|
|
177
155
|
>
|
|
178
156
|
{_('_sui.calendar.today')}
|
|
@@ -91,6 +91,8 @@
|
|
|
91
91
|
event.preventDefault();
|
|
92
92
|
event.stopPropagation();
|
|
93
93
|
|
|
94
|
+
// The button handles its own clicks, and doesn’t let them reach here
|
|
95
|
+
/* v8 ignore else */
|
|
94
96
|
if (!(/** @type {HTMLElement} */ (event.target).matches('button'))) {
|
|
95
97
|
buttonElement?.click();
|
|
96
98
|
}
|
|
@@ -122,12 +124,15 @@
|
|
|
122
124
|
|
|
123
125
|
checked = indeterminate ? true : !checked;
|
|
124
126
|
|
|
127
|
+
// The effect above keeps `checked` in step with the group, so a newly checked box is never
|
|
128
|
+
// in the group already, and a newly unchecked one is never missing from it
|
|
125
129
|
if (Array.isArray(group)) {
|
|
126
130
|
if (checked) {
|
|
131
|
+
/* v8 ignore else */
|
|
127
132
|
if (!group.includes(value)) {
|
|
128
133
|
group = [...group, value];
|
|
129
134
|
}
|
|
130
|
-
} else if (group.includes(value)) {
|
|
135
|
+
} else /* v8 ignore else */ if (group.includes(value)) {
|
|
131
136
|
group = group.filter((v) => v !== value);
|
|
132
137
|
}
|
|
133
138
|
}
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
bind:open
|
|
116
116
|
showBackdrop
|
|
117
117
|
>
|
|
118
|
-
<div bind:this={content} role="none" class=
|
|
118
|
+
<div bind:this={content} role="none" class={['content', className, size]}>
|
|
119
119
|
{#if title || showClose || header || headerExtra}
|
|
120
120
|
<div role="none" class="header">
|
|
121
121
|
{#if header}
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
</script>
|
|
72
72
|
|
|
73
73
|
<Modal bind:this={modal} {...restProps} bind:open {id} class="drawer" showBackdrop>
|
|
74
|
-
<div role="none" class=
|
|
74
|
+
<div role="none" class={['content', className, size, position, orientation]}>
|
|
75
75
|
<div role="none" class="extra-control">
|
|
76
76
|
{#if showClose === 'outside'}
|
|
77
77
|
<Button
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { _, isRTL } from '@sveltia/i18n';
|
|
14
14
|
import { onMount, untrack } from 'svelte';
|
|
15
15
|
import { searchEmojis } from './emoji.js';
|
|
16
|
+
import { getSuggestionListPosition } from './position.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* @import { EmojiAnchorRect, EmojiEntry, EmojiTrigger } from '../../typedefs';
|
|
@@ -39,10 +40,6 @@
|
|
|
39
40
|
/* eslint-enable prefer-const */
|
|
40
41
|
} = $props();
|
|
41
42
|
|
|
42
|
-
/**
|
|
43
|
-
* Width of the dropdown, also enforced in the stylesheet below.
|
|
44
|
-
*/
|
|
45
|
-
const LIST_WIDTH = 280;
|
|
46
43
|
/**
|
|
47
44
|
* How many suggestions are visible at once. The rest are reached by scrolling.
|
|
48
45
|
*/
|
|
@@ -51,11 +48,6 @@
|
|
|
51
48
|
* Height to assume until a row has been measured, so the first open is positioned sensibly.
|
|
52
49
|
*/
|
|
53
50
|
const FALLBACK_MAX_HEIGHT = 180;
|
|
54
|
-
/**
|
|
55
|
-
* Gap between the dropdown and the caret, and the minimum margin to the viewport edges.
|
|
56
|
-
*/
|
|
57
|
-
const LIST_OFFSET = 4;
|
|
58
|
-
const VIEWPORT_MARGIN = 8;
|
|
59
51
|
|
|
60
52
|
const listId = $props.id();
|
|
61
53
|
|
|
@@ -112,37 +104,17 @@
|
|
|
112
104
|
|
|
113
105
|
/**
|
|
114
106
|
* Position of the dropdown, flipped above the caret and clamped to the viewport as needed.
|
|
115
|
-
*
|
|
116
|
-
* The dropdown grows away from the start of the line, so it follows the reading direction rather
|
|
117
|
-
* than reaching back across the text: in a right-to-left layout it hangs from the shortcode’s
|
|
118
|
-
* right edge and extends leftwards, mirroring what `activatePopup()` does for anchored popups.
|
|
119
107
|
*/
|
|
120
|
-
const position = $derived
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const { innerWidth, innerHeight } = window;
|
|
126
|
-
const spaceBelow = innerHeight - anchorRect.bottom;
|
|
127
|
-
const spaceAbove = anchorRect.top;
|
|
128
|
-
const flipped = spaceBelow < listMaxHeight + VIEWPORT_MARGIN && spaceAbove > spaceBelow;
|
|
129
|
-
const rtl = isRTL();
|
|
130
|
-
const anchorLeft = rtl ? anchorRect.right - LIST_WIDTH : anchorRect.left;
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
top: flipped ? undefined : `${Math.round(anchorRect.bottom + LIST_OFFSET)}px`,
|
|
134
|
-
bottom: flipped ? `${Math.round(innerHeight - anchorRect.top + LIST_OFFSET)}px` : undefined,
|
|
135
|
-
left: `${Math.round(
|
|
136
|
-
Math.max(VIEWPORT_MARGIN, Math.min(anchorLeft, innerWidth - LIST_WIDTH - VIEWPORT_MARGIN)),
|
|
137
|
-
)}px`,
|
|
138
|
-
maxHeight: `${Math.round(
|
|
139
|
-
Math.min(
|
|
108
|
+
const position = $derived(
|
|
109
|
+
anchorRect
|
|
110
|
+
? getSuggestionListPosition({
|
|
111
|
+
anchorRect,
|
|
140
112
|
listMaxHeight,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
113
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
114
|
+
rtl: isRTL(),
|
|
115
|
+
})
|
|
116
|
+
: undefined,
|
|
117
|
+
);
|
|
146
118
|
|
|
147
119
|
/**
|
|
148
120
|
* Whether any suggestions are currently shown.
|
|
@@ -277,6 +249,8 @@
|
|
|
277
249
|
|
|
278
250
|
const row = listElement.querySelector('.option');
|
|
279
251
|
|
|
252
|
+
// The list is only mounted while there are candidates, so there is always a row
|
|
253
|
+
/* v8 ignore else */
|
|
280
254
|
if (row) {
|
|
281
255
|
const { paddingTop, paddingBottom, borderTopWidth, borderBottomWidth } =
|
|
282
256
|
getComputedStyle(listElement);
|
|
@@ -406,7 +380,7 @@
|
|
|
406
380
|
>
|
|
407
381
|
{#each candidates as entry, index (entry.emoji)}
|
|
408
382
|
<div
|
|
409
|
-
id=
|
|
383
|
+
id={`${listId}-option-${index}`}
|
|
410
384
|
role="option"
|
|
411
385
|
class="option"
|
|
412
386
|
tabindex="-1"
|
|
@@ -422,7 +396,7 @@
|
|
|
422
396
|
}}
|
|
423
397
|
>
|
|
424
398
|
<span role="none" class="emoji">{entry.emoji}</span>
|
|
425
|
-
<span role="none" class="name"
|
|
399
|
+
<span role="none" class="name">{`:${entry.name}:`}</span>
|
|
426
400
|
</div>
|
|
427
401
|
{/each}
|
|
428
402
|
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placement of the `<EmojiSuggestions>` dropdown, kept apart from the component so it can be
|
|
3
|
+
* tested against arbitrary viewports.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @import { EmojiAnchorRect } from '../../typedefs';
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Width of the dropdown, also enforced in the component’s stylesheet.
|
|
10
|
+
*/
|
|
11
|
+
export const LIST_WIDTH: 280;
|
|
12
|
+
/**
|
|
13
|
+
* Gap between the dropdown and the caret, and the minimum margin to the viewport edges.
|
|
14
|
+
*/
|
|
15
|
+
export const LIST_OFFSET: 4;
|
|
16
|
+
export const VIEWPORT_MARGIN: 8;
|
|
17
|
+
export function getSuggestionListPosition({ anchorRect, listMaxHeight, viewport, rtl }: {
|
|
18
|
+
anchorRect: EmojiAnchorRect;
|
|
19
|
+
listMaxHeight: number;
|
|
20
|
+
viewport: {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
};
|
|
24
|
+
rtl: boolean;
|
|
25
|
+
}): SuggestionListPosition;
|
|
26
|
+
export type SuggestionListPosition = {
|
|
27
|
+
/**
|
|
28
|
+
* CSS `top`, when the list hangs below the caret.
|
|
29
|
+
*/
|
|
30
|
+
top: string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* CSS `bottom`, when the list is flipped above the caret.
|
|
33
|
+
*/
|
|
34
|
+
bottom: string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* CSS `left`.
|
|
37
|
+
*/
|
|
38
|
+
left: string;
|
|
39
|
+
/**
|
|
40
|
+
* CSS `max-height`.
|
|
41
|
+
*/
|
|
42
|
+
maxHeight: string;
|
|
43
|
+
};
|
|
44
|
+
import type { EmojiAnchorRect } from '../../typedefs';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placement of the `<EmojiSuggestions>` dropdown, kept apart from the component so it can be
|
|
3
|
+
* tested against arbitrary viewports.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @import { EmojiAnchorRect } from '../../typedefs';
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Width of the dropdown, also enforced in the component’s stylesheet.
|
|
12
|
+
*/
|
|
13
|
+
export const LIST_WIDTH = 280;
|
|
14
|
+
/**
|
|
15
|
+
* Gap between the dropdown and the caret, and the minimum margin to the viewport edges.
|
|
16
|
+
*/
|
|
17
|
+
export const LIST_OFFSET = 4;
|
|
18
|
+
export const VIEWPORT_MARGIN = 8;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} SuggestionListPosition
|
|
22
|
+
* @property {string | undefined} top CSS `top`, when the list hangs below the caret.
|
|
23
|
+
* @property {string | undefined} bottom CSS `bottom`, when the list is flipped above the caret.
|
|
24
|
+
* @property {string} left CSS `left`.
|
|
25
|
+
* @property {string} maxHeight CSS `max-height`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Work out where to place the dropdown: below the caret by default, flipped above it when there
|
|
30
|
+
* is more room there and not enough below, and clamped to the viewport horizontally.
|
|
31
|
+
*
|
|
32
|
+
* The dropdown grows away from the start of the line, so it follows the reading direction rather
|
|
33
|
+
* than reaching back across the text: in a right-to-left layout it hangs from the shortcode’s
|
|
34
|
+
* right edge and extends leftwards, mirroring what `activatePopup()` does for anchored popups.
|
|
35
|
+
* @param {object} options Options.
|
|
36
|
+
* @param {EmojiAnchorRect} options.anchorRect Viewport-relative bounds of the shortcode being
|
|
37
|
+
* typed.
|
|
38
|
+
* @param {number} options.listMaxHeight The height the list would like to have, in pixels.
|
|
39
|
+
* @param {{ width: number, height: number }} options.viewport Size of the viewport in pixels.
|
|
40
|
+
* @param {boolean} options.rtl Whether the layout is right-to-left.
|
|
41
|
+
* @returns {SuggestionListPosition} Position.
|
|
42
|
+
*/
|
|
43
|
+
export const getSuggestionListPosition = ({ anchorRect, listMaxHeight, viewport, rtl }) => {
|
|
44
|
+
const { width: innerWidth, height: innerHeight } = viewport;
|
|
45
|
+
const spaceBelow = innerHeight - anchorRect.bottom;
|
|
46
|
+
const spaceAbove = anchorRect.top;
|
|
47
|
+
const flipped = spaceBelow < listMaxHeight + VIEWPORT_MARGIN && spaceAbove > spaceBelow;
|
|
48
|
+
const anchorLeft = rtl ? anchorRect.right - LIST_WIDTH : anchorRect.left;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
top: flipped ? undefined : `${Math.round(anchorRect.bottom + LIST_OFFSET)}px`,
|
|
52
|
+
bottom: flipped ? `${Math.round(innerHeight - anchorRect.top + LIST_OFFSET)}px` : undefined,
|
|
53
|
+
left: `${Math.round(
|
|
54
|
+
Math.max(VIEWPORT_MARGIN, Math.min(anchorLeft, innerWidth - LIST_WIDTH - VIEWPORT_MARGIN)),
|
|
55
|
+
)}px`,
|
|
56
|
+
maxHeight: `${Math.round(
|
|
57
|
+
Math.min(listMaxHeight, (flipped ? spaceAbove : spaceBelow) - LIST_OFFSET - VIEWPORT_MARGIN),
|
|
58
|
+
)}px`,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<script>
|
|
8
8
|
import { isRTL } from '@sveltia/i18n';
|
|
9
9
|
import { getContext } from 'svelte';
|
|
10
|
+
import { getHandleKeyAction } from './sizing.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @import { Snippet } from 'svelte';
|
|
@@ -106,6 +107,8 @@
|
|
|
106
107
|
const measureContainerSize = () => {
|
|
107
108
|
const container = element?.closest('.resizable-pane-group');
|
|
108
109
|
|
|
110
|
+
// The handle can’t be rendered outside a group; see the context check above
|
|
111
|
+
/* v8 ignore next */
|
|
109
112
|
if (!container) return 0;
|
|
110
113
|
|
|
111
114
|
return isHorizontal ? container.clientWidth : container.clientHeight;
|
|
@@ -126,6 +129,8 @@
|
|
|
126
129
|
const screenPos = isHorizontal ? screenX : screenY;
|
|
127
130
|
const pixelDelta = screenPos - startScreenPos;
|
|
128
131
|
|
|
132
|
+
// A group that has no size, because it isn’t laid out, can’t be resized
|
|
133
|
+
/* v8 ignore next */
|
|
129
134
|
if (!containerSize) return;
|
|
130
135
|
|
|
131
136
|
let percentDelta = (pixelDelta / containerSize) * 100;
|
|
@@ -194,63 +199,26 @@
|
|
|
194
199
|
if (disabled) return;
|
|
195
200
|
|
|
196
201
|
const { key, shiftKey } = event;
|
|
197
|
-
const
|
|
198
|
-
let delta = 0;
|
|
202
|
+
const action = getHandleKeyAction({ key, shiftKey, isHorizontal, rtl: isRTL() });
|
|
199
203
|
|
|
200
|
-
if (
|
|
201
|
-
event.preventDefault();
|
|
202
|
-
event.stopPropagation();
|
|
203
|
-
ctx.toggleCollapse(handleIndex);
|
|
204
|
-
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (key === 'Home') {
|
|
209
|
-
event.preventDefault();
|
|
210
|
-
event.stopPropagation();
|
|
211
|
-
// Collapse to minimum — clamped internally
|
|
212
|
-
ctx.resize(handleIndex, -100);
|
|
213
|
-
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
204
|
+
if (!action) return;
|
|
216
205
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
event.stopPropagation();
|
|
220
|
-
// Expand to maximum — clamped internally
|
|
221
|
-
ctx.resize(handleIndex, 100);
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
event.stopPropagation();
|
|
222
208
|
|
|
223
|
-
|
|
224
|
-
|
|
209
|
+
if (action.type === 'toggle') {
|
|
210
|
+
ctx.toggleCollapse(handleIndex);
|
|
225
211
|
|
|
226
|
-
if (isHorizontal) {
|
|
227
|
-
const _isRTL = isRTL();
|
|
228
|
-
|
|
229
|
-
// In RTL, Left/Right directions are visually swapped
|
|
230
|
-
if (key === 'ArrowLeft') {
|
|
231
|
-
delta = _isRTL ? step : -step;
|
|
232
|
-
} else if (key === 'ArrowRight') {
|
|
233
|
-
delta = _isRTL ? -step : step;
|
|
234
|
-
} else {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
} else if (key === 'ArrowUp') {
|
|
238
|
-
delta = -step;
|
|
239
|
-
} else if (key === 'ArrowDown') {
|
|
240
|
-
delta = step;
|
|
241
|
-
} else {
|
|
242
212
|
return;
|
|
243
213
|
}
|
|
244
214
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!keyResizing) {
|
|
215
|
+
// Home and End send an oversized delta, which gets clamped to the min/max internally
|
|
216
|
+
if (key !== 'Home' && key !== 'End' && !keyResizing) {
|
|
249
217
|
keyResizing = true;
|
|
250
218
|
onResizeStart?.();
|
|
251
219
|
}
|
|
252
220
|
|
|
253
|
-
ctx.resize(handleIndex, delta);
|
|
221
|
+
ctx.resize(handleIndex, action.delta);
|
|
254
222
|
};
|
|
255
223
|
|
|
256
224
|
/**
|
|
@@ -277,7 +245,7 @@
|
|
|
277
245
|
aria-controls={ctx.paneDefs[handleIndex]?.id}
|
|
278
246
|
aria-disabled={disabled || undefined}
|
|
279
247
|
aria-label={ariaLabel}
|
|
280
|
-
class="sui resizable-handle {className
|
|
248
|
+
class="sui resizable-handle {className}"
|
|
281
249
|
class:horizontal={isHorizontal}
|
|
282
250
|
class:vertical={!isHorizontal}
|
|
283
251
|
class:disabled
|