@pgcorp/ui-kit 0.2.0 → 0.3.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/README.md +5 -5
- package/docs/public-api.md +8 -0
- package/package.json +15 -3
- package/src/components/layout/SAppShell.vue +3 -2
- package/src/components/layout/SStack.css +9 -3
- package/src/components/layout/SStack.vue +10 -2
- package/src/components/shared/complex/STree.vue +29 -4
- package/src/components/shared/complex/treeAdapter.ts +2 -1
- package/src/components/shared/complex/types.ts +1 -12
- package/src/components/shared/containers/SLeftSidebar.vue +7 -2
- package/src/components/shared/containers/SPageHeader.css +11 -0
- package/src/components/shared/containers/SPageHeader.vue +7 -1
- package/src/components/shared/containers/SPopover.vue +2 -1
- package/src/components/shared/containers/SSidebarGroup.vue +10 -5
- package/src/components/shared/containers/sidebar.ts +2 -1
- package/src/components/shared/controls/SButton.css +13 -0
- package/src/components/shared/controls/SButton.vue +135 -29
- package/src/components/shared/controls/SCombobox.css +9 -0
- package/src/components/shared/controls/SCombobox.vue +322 -0
- package/src/components/shared/controls/SInputText.vue +15 -0
- package/src/components/shared/controls/SInteractiveSurface.vue +2 -1
- package/src/components/shared/controls/SLink.ts +1 -17
- package/src/components/shared/controls/SLink.vue +26 -2
- package/src/components/shared/controls/SListbox.vue +99 -1
- package/src/components/shared/controls/SListboxOption.vue +15 -2
- package/src/components/shared/controls/SSwitch.css +48 -0
- package/src/components/shared/controls/SSwitch.vue +109 -0
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +6 -3
- package/src/components/shared/data-display/SChip.css +59 -0
- package/src/components/shared/data-display/SChip.vue +92 -0
- package/src/components/shared/data-display/SDocBlock.vue +10 -10
- package/src/components/shared/data-display/SLinkedSystemsList.vue +3 -3
- package/src/components/shared/data-display/STable.vue +150 -14
- package/src/components/shared/data-display/STooltip.vue +2 -1
- package/src/components/shared/data-display/SVirtualList.ts +1 -16
- package/src/components/shared/data-display/SVirtualList.vue +18 -2
- package/src/components/shared/data-display/json.ts +1 -1
- package/src/components/shared/data-display/table.ts +9 -28
- package/src/components/shared/database/SDataGrid.vue +29 -8
- package/src/components/shared/database/SSqlEditor.vue +3 -2
- package/src/components/shared/database/dataGrid.ts +1 -17
- package/src/components/shared/navigation/SBottomNav.vue +3 -0
- package/src/components/shared/navigation/STabs.vue +2 -1
- package/src/composables/uiPreferencesReset.ts +2 -1
- package/src/composables/useClipboard.ts +2 -1
- package/src/composables/useSidebarPanelState.ts +3 -2
- package/src/internal/es2020.ts +50 -0
- package/src/internal/inlineTokenEditorContract.ts +4 -3
- package/src/internal/ownedAttrs.ts +24 -2
- package/src/internal/pointerInteractionLease.ts +2 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Component } from 'vue'
|
|
2
1
|
import type {
|
|
3
2
|
HistoryState,
|
|
4
3
|
LocationQueryRaw,
|
|
@@ -67,19 +66,4 @@ export type RouterLinkDestination = LinkDestinationOptions & {
|
|
|
67
66
|
/** Строгий XOR-контракт обычного destination. / Strict XOR contract for an ordinary destination. */
|
|
68
67
|
export type LinkDestination = HrefLinkDestination | RouterLinkDestination
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
export interface LinkPresentationProps {
|
|
72
|
-
readonly variant?: 'default' | 'subtle' | 'brand' | 'navigation' | 'bottom-navigation' | 'document'
|
|
73
|
-
readonly external?: boolean
|
|
74
|
-
readonly active?: boolean
|
|
75
|
-
readonly disabled?: boolean
|
|
76
|
-
readonly width?: 'fit' | 'full'
|
|
77
|
-
readonly size?: 'inherit' | 'sm' | 'md'
|
|
78
|
-
readonly underline?: 'hover' | 'always' | 'none'
|
|
79
|
-
readonly wrap?: 'normal' | 'anywhere'
|
|
80
|
-
readonly contentKind?: 'text' | 'identifier'
|
|
81
|
-
readonly leadingIcon?: Component
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Полный discriminated public props contract SLink. / Complete discriminated public SLink props contract. */
|
|
85
|
-
export type SLinkProps = LinkDestination & LinkPresentationProps
|
|
69
|
+
export type { LinkPresentationProps, Props as SLinkProps } from './SLink.vue'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
+
type Component,
|
|
3
4
|
computed,
|
|
4
5
|
type VNodeChild,
|
|
5
6
|
} from 'vue'
|
|
@@ -12,13 +13,36 @@ import {
|
|
|
12
13
|
type PassiveContentContract,
|
|
13
14
|
} from '../../../internal/passiveContentContract'
|
|
14
15
|
import type {
|
|
15
|
-
|
|
16
|
+
LinkBrowsingTarget,
|
|
17
|
+
LinkRouterTarget,
|
|
16
18
|
} from './SLink'
|
|
17
19
|
|
|
18
20
|
defineOptions({ inheritAttrs: false })
|
|
19
21
|
|
|
22
|
+
/** Canonical presentation/state API owned by SLink. */
|
|
23
|
+
export interface LinkPresentationProps {
|
|
24
|
+
readonly variant?: 'default' | 'subtle' | 'brand' | 'navigation' | 'bottom-navigation' | 'document'
|
|
25
|
+
readonly external?: boolean
|
|
26
|
+
readonly active?: boolean
|
|
27
|
+
readonly disabled?: boolean
|
|
28
|
+
readonly width?: 'fit' | 'full'
|
|
29
|
+
readonly size?: 'inherit' | 'sm' | 'md'
|
|
30
|
+
readonly underline?: 'hover' | 'always' | 'none'
|
|
31
|
+
readonly wrap?: 'normal' | 'anywhere'
|
|
32
|
+
readonly contentKind?: 'text' | 'identifier'
|
|
33
|
+
readonly leadingIcon?: Component
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface LinkDestinationOptions {
|
|
37
|
+
readonly target?: LinkBrowsingTarget
|
|
38
|
+
readonly rel?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
/** Exact href/to XOR plus the canonical link presentation API. */
|
|
21
|
-
export type Props =
|
|
42
|
+
export type Props = LinkPresentationProps & LinkDestinationOptions & (
|
|
43
|
+
| { readonly href: string; readonly to?: never }
|
|
44
|
+
| { readonly href?: never; readonly to: LinkRouterTarget }
|
|
45
|
+
)
|
|
22
46
|
|
|
23
47
|
const props = withDefaults(defineProps<Props>(), {
|
|
24
48
|
href: undefined,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
:data-variant="resolvedVariant"
|
|
7
7
|
:data-invalid="resolvedInvalid || undefined"
|
|
8
8
|
:data-readonly="resolvedReadonly || undefined"
|
|
9
|
+
:data-focus-mode="resolvedFocusMode"
|
|
9
10
|
role="listbox"
|
|
10
11
|
:aria-label="resolvedLabel"
|
|
11
12
|
:aria-multiselectable="resolvedSelectionMode === 'multiple' || undefined"
|
|
@@ -17,23 +18,31 @@
|
|
|
17
18
|
<SListboxOption
|
|
18
19
|
v-if="resolvedClearOptionLabel"
|
|
19
20
|
:registration-token="CLEAR_OPTION_TOKEN"
|
|
21
|
+
:id="optionId(CLEAR_OPTION_TOKEN)"
|
|
20
22
|
:label="resolvedClearOptionLabel"
|
|
21
23
|
:title="resolvedClearOptionLabel"
|
|
22
24
|
:selected="modelValue === null"
|
|
23
25
|
:disabled="resolvedDisabled"
|
|
24
26
|
:appearance="optionAppearance"
|
|
27
|
+
:active="activeToken === CLEAR_OPTION_TOKEN"
|
|
28
|
+
:focus-on-select="resolvedFocusMode === 'roving'"
|
|
29
|
+
@hover="setActiveToken(CLEAR_OPTION_TOKEN)"
|
|
25
30
|
@select="clearSelection"
|
|
26
31
|
/>
|
|
27
32
|
<template v-for="entry in inventory.entries" :key="entry.token">
|
|
28
33
|
<SListboxOption
|
|
29
34
|
v-if="entry.kind === 'option'"
|
|
30
35
|
:registration-token="entry.record.token"
|
|
36
|
+
:id="optionId(entry.record.token)"
|
|
31
37
|
:label="entry.record.option.label"
|
|
32
38
|
:title="entry.record.option.title ?? entry.record.option.label"
|
|
33
39
|
:selected="selectedTokens.has(entry.record.token)"
|
|
34
40
|
:disabled="resolvedDisabled || entry.record.option.disabled"
|
|
35
41
|
:marker="entry.record.option.marker"
|
|
36
42
|
:appearance="optionAppearance"
|
|
43
|
+
:active="activeToken === entry.record.token"
|
|
44
|
+
:focus-on-select="resolvedFocusMode === 'roving'"
|
|
45
|
+
@hover="setActiveToken(entry.record.token)"
|
|
37
46
|
@select="selectOption(entry.record.token)"
|
|
38
47
|
/>
|
|
39
48
|
<SListboxGroup
|
|
@@ -46,12 +55,16 @@
|
|
|
46
55
|
v-for="record in entry.records"
|
|
47
56
|
:key="record.token"
|
|
48
57
|
:registration-token="record.token"
|
|
58
|
+
:id="optionId(record.token)"
|
|
49
59
|
:label="record.option.label"
|
|
50
60
|
:title="record.option.title ?? record.option.label"
|
|
51
61
|
:selected="selectedTokens.has(record.token)"
|
|
52
62
|
:disabled="resolvedDisabled || record.option.disabled"
|
|
53
63
|
:marker="record.option.marker"
|
|
54
64
|
:appearance="optionAppearance"
|
|
65
|
+
:active="activeToken === record.token"
|
|
66
|
+
:focus-on-select="resolvedFocusMode === 'roving'"
|
|
67
|
+
@hover="setActiveToken(record.token)"
|
|
55
68
|
@select="selectOption(record.token)"
|
|
56
69
|
/>
|
|
57
70
|
</SListboxGroup>
|
|
@@ -73,6 +86,8 @@ import type {
|
|
|
73
86
|
} from '../_internal/selectionContract'
|
|
74
87
|
|
|
75
88
|
export type SListboxVariant = 'select' | 'color'
|
|
89
|
+
export type SListboxFocusMode = 'roving' | 'activedescendant'
|
|
90
|
+
export type SListboxMoveIntent = 'next' | 'previous' | 'first' | 'last'
|
|
76
91
|
export type SListboxOption<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionOption<TKey>
|
|
77
92
|
export type SListboxOptionGroup<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionOptionGroup<TKey>
|
|
78
93
|
export type SListboxEntry<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionEntry<TKey>
|
|
@@ -106,6 +121,8 @@ interface SharedProps<TKey extends PublicSelectionKey> {
|
|
|
106
121
|
emptyLabel?: string
|
|
107
122
|
clearOptionLabel?: string
|
|
108
123
|
id?: string
|
|
124
|
+
focusMode?: SListboxFocusMode
|
|
125
|
+
activeValue?: TKey | null
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
export type SListboxProps<
|
|
@@ -154,9 +171,12 @@ const props = withDefaults(defineProps<ComponentProps<TKey>>(), {
|
|
|
154
171
|
emptyLabel: 'Нет доступных вариантов',
|
|
155
172
|
clearOptionLabel: undefined,
|
|
156
173
|
id: undefined,
|
|
174
|
+
focusMode: 'roving',
|
|
175
|
+
activeValue: undefined,
|
|
157
176
|
})
|
|
158
177
|
const emit = defineEmits<{
|
|
159
178
|
'update:modelValue': [value: TKey | null] | [value: readonly TKey[]]
|
|
179
|
+
'update:activeValue': [value: TKey | null]
|
|
160
180
|
'option-keydown': [payload: SListboxOptionKeydownPayload<TKey>]
|
|
161
181
|
escape: []
|
|
162
182
|
tab: [event: KeyboardEvent]
|
|
@@ -187,6 +207,9 @@ const contract = computed(() => {
|
|
|
187
207
|
disabled: validateBoolean('SListbox', 'disabled', props.disabled),
|
|
188
208
|
readonly: validateBoolean('SListbox', 'readonly', props.readonly),
|
|
189
209
|
invalid: validateBoolean('SListbox', 'invalid', props.invalid),
|
|
210
|
+
focusMode: validateExactString(
|
|
211
|
+
'SListbox', 'focusMode', props.focusMode, ['roving', 'activedescendant'] as const,
|
|
212
|
+
),
|
|
190
213
|
selectionMode,
|
|
191
214
|
clearOptionLabel,
|
|
192
215
|
}
|
|
@@ -199,6 +222,7 @@ const resolvedDisabled = computed(() => contract.value.disabled)
|
|
|
199
222
|
const resolvedReadonly = computed(() => contract.value.readonly)
|
|
200
223
|
const resolvedInvalid = computed(() => contract.value.invalid)
|
|
201
224
|
const resolvedSelectionMode = computed(() => contract.value.selectionMode)
|
|
225
|
+
const resolvedFocusMode = computed(() => contract.value.focusMode)
|
|
202
226
|
const resolvedClearOptionLabel = computed(() => contract.value.clearOptionLabel)
|
|
203
227
|
const selectionState = computed(() => {
|
|
204
228
|
const resolvedInventory = resolveSelectionInventory<TKey>('SListbox', props.options)
|
|
@@ -213,6 +237,25 @@ const inventory = computed(() => selectionState.value.inventory)
|
|
|
213
237
|
const selectedTokens = computed(() => selectionState.value.selectedTokens)
|
|
214
238
|
const optionAppearance = computed(() => resolvedVariant.value === 'color' ? 'color' : 'select')
|
|
215
239
|
const CLEAR_OPTION_TOKEN = 'clear-option'
|
|
240
|
+
const activeToken = computed(() => {
|
|
241
|
+
if (resolvedFocusMode.value === 'roving') return undefined
|
|
242
|
+
if (resolvedSelectionMode.value !== 'single') {
|
|
243
|
+
throw new Error('SListbox: activedescendant focus mode supports single selection only')
|
|
244
|
+
}
|
|
245
|
+
if (props.activeValue === undefined) {
|
|
246
|
+
throw new TypeError('SListbox: activeValue is required in activedescendant focus mode')
|
|
247
|
+
}
|
|
248
|
+
if (props.activeValue === null) return null
|
|
249
|
+
const token = selectionKeyToken('SListbox', props.activeValue, 'activeValue')
|
|
250
|
+
if (!inventory.value.optionsByToken.has(token)) {
|
|
251
|
+
throw new Error('SListbox: activeValue does not match any option')
|
|
252
|
+
}
|
|
253
|
+
return token
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
function optionId(token: string): string {
|
|
257
|
+
return `${resolvedId.value}-option-${encodeURIComponent(token)}`
|
|
258
|
+
}
|
|
216
259
|
|
|
217
260
|
function emitModelValue(value: TKey | null | readonly TKey[]): void {
|
|
218
261
|
if (contract.value.selectionMode === 'multiple') {
|
|
@@ -281,6 +324,7 @@ function enabledRegistrations(): ListboxOptionRegistration[] {
|
|
|
281
324
|
function initializeTabstop(): void {
|
|
282
325
|
const options = enabledRegistrations()
|
|
283
326
|
for (const option of registrations) option.element.tabIndex = -1
|
|
327
|
+
if (resolvedFocusMode.value === 'activedescendant') return
|
|
284
328
|
const selected = options.find((option) => option.selected())
|
|
285
329
|
const target = selected ?? options[0]
|
|
286
330
|
if (target) target.element.tabIndex = 0
|
|
@@ -300,6 +344,56 @@ function focusOption(target: ListboxOptionRegistration, options = enabledRegistr
|
|
|
300
344
|
target.element.focus()
|
|
301
345
|
}
|
|
302
346
|
|
|
347
|
+
function setActiveToken(token: string): void {
|
|
348
|
+
if (resolvedFocusMode.value !== 'activedescendant') return
|
|
349
|
+
const target = enabledRegistrations().find((option) => option.token === token)
|
|
350
|
+
if (!target) return
|
|
351
|
+
emit('update:activeValue', optionValue(target))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function activeRegistration(options: ListboxOptionRegistration[]): ListboxOptionRegistration | undefined {
|
|
355
|
+
const token = activeToken.value
|
|
356
|
+
return token === undefined || token === null
|
|
357
|
+
? undefined
|
|
358
|
+
: options.find((option) => option.token === token)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function initializeActive(): void {
|
|
362
|
+
if (resolvedFocusMode.value !== 'activedescendant') {
|
|
363
|
+
throw new Error('SListbox: initializeActive is available only in activedescendant focus mode')
|
|
364
|
+
}
|
|
365
|
+
const options = enabledRegistrations()
|
|
366
|
+
const target = activeRegistration(options) ?? options.find((option) => option.selected()) ?? options[0]
|
|
367
|
+
if (target) emit('update:activeValue', optionValue(target))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function moveActive(intent: SListboxMoveIntent): void {
|
|
371
|
+
if (resolvedFocusMode.value !== 'activedescendant') {
|
|
372
|
+
throw new Error('SListbox: moveActive is available only in activedescendant focus mode')
|
|
373
|
+
}
|
|
374
|
+
const options = enabledRegistrations()
|
|
375
|
+
if (options.length === 0) return
|
|
376
|
+
const current = activeRegistration(options)
|
|
377
|
+
const currentIndex = current === undefined ? -1 : options.indexOf(current)
|
|
378
|
+
const nextIndex = intent === 'first'
|
|
379
|
+
? 0
|
|
380
|
+
: intent === 'last'
|
|
381
|
+
? options.length - 1
|
|
382
|
+
: intent === 'next'
|
|
383
|
+
? (Math.max(currentIndex, -1) + 1) % options.length
|
|
384
|
+
: (currentIndex <= 0 ? options.length : currentIndex) - 1
|
|
385
|
+
const target = options[nextIndex]
|
|
386
|
+
if (target) emit('update:activeValue', optionValue(target))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function selectActive(): void {
|
|
390
|
+
if (resolvedFocusMode.value !== 'activedescendant') {
|
|
391
|
+
throw new Error('SListbox: selectActive is available only in activedescendant focus mode')
|
|
392
|
+
}
|
|
393
|
+
if (resolvedReadonly.value || resolvedDisabled.value) return
|
|
394
|
+
activeRegistration(enabledRegistrations())?.activate()
|
|
395
|
+
}
|
|
396
|
+
|
|
303
397
|
function currentOption(
|
|
304
398
|
options: ListboxOptionRegistration[],
|
|
305
399
|
event: KeyboardEvent,
|
|
@@ -319,6 +413,7 @@ function optionValue(registration: ListboxOptionRegistration): TKey | null {
|
|
|
319
413
|
}
|
|
320
414
|
|
|
321
415
|
function handleKeydown(event: KeyboardEvent): void {
|
|
416
|
+
if (resolvedFocusMode.value === 'activedescendant') return
|
|
322
417
|
if (event.key === 'Tab') {
|
|
323
418
|
emit('tab', event)
|
|
324
419
|
return
|
|
@@ -376,13 +471,16 @@ function handleKeydown(event: KeyboardEvent): void {
|
|
|
376
471
|
}
|
|
377
472
|
|
|
378
473
|
function focusSelectedOrFirst(): void {
|
|
474
|
+
if (resolvedFocusMode.value !== 'roving') {
|
|
475
|
+
throw new Error('SListbox: focusSelectedOrFirst is available only in roving focus mode')
|
|
476
|
+
}
|
|
379
477
|
const options = enabledRegistrations()
|
|
380
478
|
const target = options.find((option) => option.selected()) ?? options[0]
|
|
381
479
|
if (target) focusOption(target, options)
|
|
382
480
|
}
|
|
383
481
|
|
|
384
482
|
onBeforeUnmount(() => { if (typeaheadTimer) clearTimeout(typeaheadTimer) })
|
|
385
|
-
defineExpose({ focusSelectedOrFirst })
|
|
483
|
+
defineExpose({ focusSelectedOrFirst, initializeActive, moveActive, selectActive })
|
|
386
484
|
</script>
|
|
387
485
|
|
|
388
486
|
<style lang="postcss" src="./SListbox.css" scoped></style>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<li
|
|
3
3
|
ref="optionRef"
|
|
4
|
+
:id="id"
|
|
4
5
|
class="s-listbox-option__control"
|
|
5
6
|
:data-appearance="appearance"
|
|
6
|
-
:data-focused="focused || undefined"
|
|
7
|
+
:data-focused="focused || active || undefined"
|
|
7
8
|
:title="title ?? label"
|
|
8
9
|
:aria-label="appearance === 'color' ? label : undefined"
|
|
9
10
|
role="option"
|
|
@@ -12,6 +13,8 @@
|
|
|
12
13
|
:tabindex="-1"
|
|
13
14
|
@focus="focused = true"
|
|
14
15
|
@blur="focused = false"
|
|
16
|
+
@mousedown="onMousedown"
|
|
17
|
+
@mouseenter="emit('hover')"
|
|
15
18
|
@click.stop="select"
|
|
16
19
|
>
|
|
17
20
|
<span class="s-listbox-option__main" :data-appearance="appearance">
|
|
@@ -42,6 +45,9 @@ export interface Props {
|
|
|
42
45
|
appearance?: 'select' | 'color'
|
|
43
46
|
marker?: MarkerContract
|
|
44
47
|
title?: string
|
|
48
|
+
id?: string
|
|
49
|
+
active?: boolean
|
|
50
|
+
focusOnSelect?: boolean
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
defineOptions({ inheritAttrs: false })
|
|
@@ -51,10 +57,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
51
57
|
appearance: 'select',
|
|
52
58
|
marker: undefined,
|
|
53
59
|
title: undefined,
|
|
60
|
+
id: undefined,
|
|
61
|
+
active: false,
|
|
62
|
+
focusOnSelect: true,
|
|
54
63
|
})
|
|
55
64
|
|
|
56
65
|
const emit = defineEmits<{
|
|
57
66
|
select: []
|
|
67
|
+
hover: []
|
|
58
68
|
}>()
|
|
59
69
|
useInteractiveLeafRegistration({ owner: 'SListboxOption' })
|
|
60
70
|
const context = inject(listboxContextKey)
|
|
@@ -64,9 +74,12 @@ const optionRef = ref<HTMLLIElement | null>(null)
|
|
|
64
74
|
let unregister: (() => void) | undefined
|
|
65
75
|
const select = (): void => {
|
|
66
76
|
if (props.disabled) return
|
|
67
|
-
optionRef.value?.focus()
|
|
77
|
+
if (props.focusOnSelect) optionRef.value?.focus()
|
|
68
78
|
emit('select')
|
|
69
79
|
}
|
|
80
|
+
const onMousedown = (event: MouseEvent): void => {
|
|
81
|
+
if (!props.focusOnSelect) event.preventDefault()
|
|
82
|
+
}
|
|
70
83
|
onMounted(() => {
|
|
71
84
|
const element = optionRef.value
|
|
72
85
|
if (!(element instanceof HTMLLIElement)) throw new Error('SListboxOption: option element is unavailable')
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-switch-control {
|
|
4
|
+
@apply relative inline-flex shrink-0 items-center;
|
|
5
|
+
width: 2.5rem;
|
|
6
|
+
height: 1.5rem;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.s-switch-input {
|
|
10
|
+
@apply absolute inset-0 z-10 m-0 cursor-pointer opacity-0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.s-switch-track {
|
|
14
|
+
@apply flex h-5 w-9 items-center rounded-full border border-surface-300 bg-surface-200 p-0.5 transition-colors;
|
|
15
|
+
@apply dark:border-surface-600 dark:bg-surface-700;
|
|
16
|
+
transition-duration: var(--s-motion-standard);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.s-switch-thumb {
|
|
20
|
+
@apply block h-3.5 w-3.5 rounded-full bg-surface-0 shadow-sm transition-transform;
|
|
21
|
+
@apply dark:bg-surface-100;
|
|
22
|
+
transition-duration: var(--s-motion-standard);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.s-switch-input:checked + .s-switch-track {
|
|
26
|
+
@apply border-primary-500 bg-primary-500 dark:border-primary-400 dark:bg-primary-400;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.s-switch-input:checked + .s-switch-track > .s-switch-thumb {
|
|
30
|
+
transform: translateX(1rem);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.s-switch-input:not(:disabled):hover + .s-switch-track {
|
|
34
|
+
@apply border-primary-500 dark:border-primary-400;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.s-switch-input:focus-visible + .s-switch-track {
|
|
38
|
+
outline: var(--s-focus-ring-width) solid var(--s-focus-ring-strong-color);
|
|
39
|
+
outline-offset: var(--s-focus-ring-offset);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.s-switch-input:disabled {
|
|
43
|
+
@apply cursor-not-allowed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.s-switch-input:disabled + .s-switch-track {
|
|
47
|
+
@apply border-surface-200 bg-surface-100 opacity-70 dark:border-surface-700 dark:bg-surface-800;
|
|
48
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<SField
|
|
3
|
+
:control-id="inputId"
|
|
4
|
+
:label="label"
|
|
5
|
+
:required="required"
|
|
6
|
+
:help-text="helpText"
|
|
7
|
+
:error-message="errorMessage"
|
|
8
|
+
:invalid="invalid"
|
|
9
|
+
:disabled="disabled"
|
|
10
|
+
:readonly="readonly"
|
|
11
|
+
layout="inline"
|
|
12
|
+
>
|
|
13
|
+
<template #default="field">
|
|
14
|
+
<span class="s-switch-control">
|
|
15
|
+
<input
|
|
16
|
+
v-bind="ownedAttrs.bindings()"
|
|
17
|
+
:id="field.controlId"
|
|
18
|
+
type="checkbox"
|
|
19
|
+
role="switch"
|
|
20
|
+
class="s-switch-input"
|
|
21
|
+
:checked="checkedValue"
|
|
22
|
+
:disabled="field.disabled"
|
|
23
|
+
:required="field.required"
|
|
24
|
+
:aria-checked="checkedValue"
|
|
25
|
+
:aria-invalid="field.invalid || undefined"
|
|
26
|
+
:aria-readonly="field.readonly || undefined"
|
|
27
|
+
:aria-describedby="mergeIdReferences(ownedAttrs.string('aria-describedby'), field.describedBy)"
|
|
28
|
+
@click.stop="onClick"
|
|
29
|
+
@change="onChange"
|
|
30
|
+
/>
|
|
31
|
+
<span class="s-switch-track" aria-hidden="true">
|
|
32
|
+
<span class="s-switch-thumb" />
|
|
33
|
+
</span>
|
|
34
|
+
</span>
|
|
35
|
+
</template>
|
|
36
|
+
</SField>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { computed, useId } from 'vue'
|
|
41
|
+
import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
42
|
+
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
43
|
+
import { resolveOptionalDomId } from '../../../internal/runtimeContract'
|
|
44
|
+
import SField from './SField.vue'
|
|
45
|
+
|
|
46
|
+
defineOptions({ inheritAttrs: false })
|
|
47
|
+
|
|
48
|
+
export interface Props {
|
|
49
|
+
modelValue: boolean
|
|
50
|
+
label?: string
|
|
51
|
+
disabled?: boolean
|
|
52
|
+
readonly?: boolean
|
|
53
|
+
required?: boolean
|
|
54
|
+
invalid?: boolean
|
|
55
|
+
helpText?: string
|
|
56
|
+
errorMessage?: string
|
|
57
|
+
id?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
61
|
+
label: undefined,
|
|
62
|
+
disabled: false,
|
|
63
|
+
readonly: false,
|
|
64
|
+
required: false,
|
|
65
|
+
invalid: false,
|
|
66
|
+
helpText: undefined,
|
|
67
|
+
errorMessage: undefined,
|
|
68
|
+
id: undefined,
|
|
69
|
+
})
|
|
70
|
+
const emit = defineEmits<{
|
|
71
|
+
'update:modelValue': [value: boolean]
|
|
72
|
+
click: [event: MouseEvent]
|
|
73
|
+
}>()
|
|
74
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SSwitch', owner: 'native switch input' })
|
|
75
|
+
useInteractiveLeafRegistration({ owner: 'SSwitch' })
|
|
76
|
+
|
|
77
|
+
const generatedInputId = useId()
|
|
78
|
+
const inputId = computed(() => resolveOptionalDomId(
|
|
79
|
+
'SSwitch',
|
|
80
|
+
'id',
|
|
81
|
+
props.id,
|
|
82
|
+
`s-switch-${generatedInputId}`,
|
|
83
|
+
))
|
|
84
|
+
const checkedValue = computed(() => {
|
|
85
|
+
if (typeof props.modelValue !== 'boolean') {
|
|
86
|
+
throw new TypeError('SSwitch: modelValue must be boolean')
|
|
87
|
+
}
|
|
88
|
+
return props.modelValue
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
function onChange(event: Event): void {
|
|
92
|
+
if (props.readonly) return
|
|
93
|
+
const target = event.currentTarget
|
|
94
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
95
|
+
throw new TypeError('SSwitch: change event owner must be the native switch input')
|
|
96
|
+
}
|
|
97
|
+
emit('update:modelValue', target.checked)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function onClick(event: MouseEvent): void {
|
|
101
|
+
if (props.readonly) {
|
|
102
|
+
event.preventDefault()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
emit('click', event)
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<style lang="postcss" src="./SSwitch.css" scoped></style>
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createTextVNode,
|
|
5
5
|
defineComponent,
|
|
6
6
|
h,
|
|
7
|
+
type Component,
|
|
7
8
|
type ComponentPublicInstance,
|
|
8
9
|
type PropType,
|
|
9
10
|
} from 'vue'
|
|
@@ -14,7 +15,9 @@ import type { InlineTokenEditorSegmentContract } from '../../../../internal/inli
|
|
|
14
15
|
import type { OwnedAttributeBindings } from '../../../../internal/ownedAttrs'
|
|
15
16
|
import { useInteractiveLeafRegistration } from '../../../../internal/passiveContentContract'
|
|
16
17
|
import { validateBoundedInteger } from '../../../../internal/runtimeContract'
|
|
17
|
-
import SButton, { type
|
|
18
|
+
import SButton, { type SButtonActionProps } from '../SButton.vue'
|
|
19
|
+
|
|
20
|
+
const actionButtonComponent: Component = SButton
|
|
18
21
|
|
|
19
22
|
/** Текст визуального caret helper. / Visual caret-helper text. */
|
|
20
23
|
export const INLINE_TOKEN_EDITOR_VISUAL_CARET_TEXT = '\u200B'
|
|
@@ -35,7 +38,7 @@ export type InlineTokenElementRef = (
|
|
|
35
38
|
refs: Record<string, unknown>,
|
|
36
39
|
) => void
|
|
37
40
|
|
|
38
|
-
interface InlineTokenActionButtonBindings extends
|
|
41
|
+
interface InlineTokenActionButtonBindings extends SButtonActionProps {
|
|
39
42
|
readonly 'aria-label': string
|
|
40
43
|
readonly title?: string
|
|
41
44
|
readonly onClick: (event: MouseEvent) => void
|
|
@@ -161,7 +164,7 @@ export default defineComponent({
|
|
|
161
164
|
onClick: (event) => emitTokenAction(event, action, segment),
|
|
162
165
|
onKeydown: stopKeyPropagation,
|
|
163
166
|
}
|
|
164
|
-
return h(
|
|
167
|
+
return h(actionButtonComponent, { ...bindings, key: action })
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
return () => h('div', {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-chip {
|
|
4
|
+
@apply inline-flex min-w-0 items-center rounded-full font-medium;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.s-chip[data-size='md'] {
|
|
8
|
+
@apply min-h-7 gap-1.5 px-2.5 text-xs;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.s-chip[data-size='md'][data-closable='true'] {
|
|
12
|
+
@apply pr-1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.s-chip[data-size='sm'] {
|
|
16
|
+
@apply min-h-6 gap-1 px-2 text-[11px] leading-4;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.s-chip[data-size='sm'][data-closable='true'] {
|
|
20
|
+
@apply pr-0.5;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.s-chip__icon {
|
|
24
|
+
@apply inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.s-chip__icon-graphic {
|
|
28
|
+
@apply h-full w-full;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.s-chip__label {
|
|
32
|
+
@apply min-w-0 overflow-hidden text-ellipsis whitespace-nowrap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.s-chip[data-disabled='true'] {
|
|
36
|
+
@apply opacity-60;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.s-chip[data-severity='primary'] {
|
|
40
|
+
@apply bg-primary-100 text-primary-800 dark:bg-primary-950 dark:text-primary-50;
|
|
41
|
+
}
|
|
42
|
+
.s-chip[data-severity='secondary'] {
|
|
43
|
+
@apply bg-surface-100 text-surface-800 dark:bg-surface-800 dark:text-surface-50;
|
|
44
|
+
}
|
|
45
|
+
.s-chip[data-severity='success'] {
|
|
46
|
+
@apply bg-success-100 text-success-800 dark:bg-success-950 dark:text-success-50;
|
|
47
|
+
}
|
|
48
|
+
.s-chip[data-severity='info'] {
|
|
49
|
+
@apply bg-info-100 text-info-800 dark:bg-info-950 dark:text-info-50;
|
|
50
|
+
}
|
|
51
|
+
.s-chip[data-severity='warn'] {
|
|
52
|
+
@apply bg-warn-100 text-warn-800 dark:bg-warn-950 dark:text-warn-50;
|
|
53
|
+
}
|
|
54
|
+
.s-chip[data-severity='danger'] {
|
|
55
|
+
@apply bg-danger-100 text-danger-800 dark:bg-danger-950 dark:text-danger-50;
|
|
56
|
+
}
|
|
57
|
+
.s-chip[data-severity='contrast'] {
|
|
58
|
+
@apply bg-surface-800 text-surface-100 dark:bg-surface-200 dark:text-surface-800;
|
|
59
|
+
}
|