@pgcorp/ui-kit 0.5.0 → 0.6.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.
- package/package.json +1 -1
- package/src/components/shared/_internal/SHeaderSurface.css +98 -0
- package/src/components/shared/_internal/SHeaderSurface.vue +80 -0
- package/src/components/shared/_internal/useRadioGroup.ts +106 -0
- package/src/components/shared/containers/SPageHeader.vue +13 -20
- package/src/components/shared/controls/SCheckbox.vue +14 -43
- package/src/components/shared/controls/SChoiceCards.vue +16 -39
- package/src/components/shared/controls/SField.css +0 -12
- package/src/components/shared/controls/SField.vue +14 -10
- package/src/components/shared/controls/SFieldGroup.css +1 -19
- package/src/components/shared/controls/SFieldGroup.vue +13 -10
- package/src/components/shared/controls/SFieldLabel.css +0 -1
- package/src/components/shared/controls/SFieldLabel.vue +2 -1
- package/src/components/shared/controls/SSegmentedControl.vue +15 -43
- package/src/components/shared/controls/SSwitch.vue +14 -32
- package/src/components/shared/data-display/SBadge.vue +41 -18
- package/src/components/shared/data-display/SChip.vue +22 -10
- package/src/components/shared/data-display/SSectionHeader.vue +17 -21
- package/src/components/shared/database/SDataGrid.vue +13 -0
- package/src/internal/fieldPresentation.ts +28 -0
- package/src/internal/pillSurface.ts +92 -0
- package/src/internal/useBinaryInput.ts +76 -0
- package/src/styles/style.css +114 -0
- package/src/styles/tokens.css +31 -0
- package/src/components/shared/containers/SPageHeader.css +0 -82
- package/src/components/shared/data-display/SBadge.css +0 -73
- package/src/components/shared/data-display/SChip.css +0 -59
- package/src/components/shared/data-display/SSectionHeader.css +0 -84
package/package.json
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-page-header,
|
|
4
|
+
.s-section-header {
|
|
5
|
+
@apply flex min-w-0 items-start justify-between;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.s-page-header {
|
|
9
|
+
@apply gap-4 border border-surface-200 bg-surface-0 p-5 shadow-sm;
|
|
10
|
+
@apply dark:border-surface-700 dark:bg-surface-900;
|
|
11
|
+
border-radius: var(--radius-sm);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.s-section-header { @apply flex-wrap gap-3; }
|
|
15
|
+
|
|
16
|
+
.s-page-header[data-appearance='plain'] {
|
|
17
|
+
@apply border-x-0 border-t-0 bg-transparent px-0 pt-0 shadow-none dark:bg-transparent;
|
|
18
|
+
border-radius: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.s-page-header[data-size='compact'] { @apply p-4; }
|
|
22
|
+
.s-page-header[data-appearance='plain'][data-size='compact'] { @apply px-0 pt-0; }
|
|
23
|
+
|
|
24
|
+
.s-page-header[data-appearance='bare'] {
|
|
25
|
+
@apply border-0 bg-transparent p-0 shadow-none dark:bg-transparent;
|
|
26
|
+
border-radius: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.s-section-header[data-spacing-after='sm'] { @apply mb-2; }
|
|
30
|
+
.s-section-header[data-spacing-after='md'] { @apply mb-3; }
|
|
31
|
+
.s-section-header[data-divider='true'] { @apply border-b border-surface-200 pb-3 dark:border-surface-700; }
|
|
32
|
+
.s-section-header[data-padding='sm'] { @apply p-2; }
|
|
33
|
+
.s-section-header[data-padding='md'] { @apply p-4; }
|
|
34
|
+
.s-section-header[data-divider='true'][data-padding='sm'] { @apply pb-2; }
|
|
35
|
+
.s-section-header[data-divider='true'][data-padding='md'] { @apply pb-4; }
|
|
36
|
+
|
|
37
|
+
.s-page-header__copy,
|
|
38
|
+
.s-section-header__copy { @apply min-w-0; }
|
|
39
|
+
.s-section-header__copy { @apply flex-1; }
|
|
40
|
+
|
|
41
|
+
.s-page-header__eyebrow,
|
|
42
|
+
.s-section-header__eyebrow { @apply text-xs uppercase text-surface-500 dark:text-surface-400; }
|
|
43
|
+
.s-page-header__eyebrow { @apply font-medium; }
|
|
44
|
+
.s-section-header__eyebrow { @apply font-semibold; }
|
|
45
|
+
|
|
46
|
+
.s-page-header__title,
|
|
47
|
+
.s-section-header__title { @apply m-0 font-semibold text-surface-900 dark:text-surface-50; }
|
|
48
|
+
.s-page-header__title { @apply text-2xl leading-tight; }
|
|
49
|
+
|
|
50
|
+
.s-page-header__title[data-presentation='assistive'] {
|
|
51
|
+
position: absolute;
|
|
52
|
+
width: 1px;
|
|
53
|
+
height: 1px;
|
|
54
|
+
padding: 0;
|
|
55
|
+
margin: -1px;
|
|
56
|
+
clip-path: inset(50%);
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
border: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.s-page-header[data-size='compact'] .s-page-header__title { @apply text-xl; }
|
|
62
|
+
.s-section-header__title { @apply break-words text-base; }
|
|
63
|
+
.s-section-header[data-size='sm'] .s-section-header__title { @apply text-sm; }
|
|
64
|
+
.s-section-header[data-variant='display'] .s-section-header__title {
|
|
65
|
+
@apply text-xl font-semibold uppercase tracking-[0.22em] text-surface-700 dark:text-surface-200;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.s-page-header__eyebrow + .s-page-header__title,
|
|
69
|
+
.s-section-header__eyebrow + .s-section-header__title { @apply mt-1; }
|
|
70
|
+
|
|
71
|
+
.s-page-header__description,
|
|
72
|
+
.s-section-header__description,
|
|
73
|
+
.s-section-header__meta { @apply text-surface-500 dark:text-surface-400; }
|
|
74
|
+
.s-page-header__description { @apply mt-2 max-w-4xl text-sm; }
|
|
75
|
+
.s-section-header__description,
|
|
76
|
+
.s-section-header__meta {
|
|
77
|
+
@apply mt-1 min-w-0 break-words text-xs;
|
|
78
|
+
overflow-wrap: anywhere;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.s-page-header__actions,
|
|
82
|
+
.s-section-header__trailing,
|
|
83
|
+
.s-section-header__badges,
|
|
84
|
+
.s-section-header__actions { @apply flex min-w-0 flex-wrap items-center justify-end gap-2; }
|
|
85
|
+
.s-page-header__actions { @apply shrink-0; }
|
|
86
|
+
.s-page-header[data-actions-layout='stacked'] { @apply flex-col; }
|
|
87
|
+
|
|
88
|
+
.s-page-header[data-actions-layout='stacked'] .s-page-header__actions,
|
|
89
|
+
.s-section-header[data-trailing-layout='stacked'] .s-section-header__trailing,
|
|
90
|
+
.s-section-header[data-trailing-layout='stacked'] .s-section-header__actions { @apply w-full justify-start; }
|
|
91
|
+
|
|
92
|
+
@media (max-width: 639px) {
|
|
93
|
+
.s-page-header[data-actions-layout='responsive'] { @apply flex-col; }
|
|
94
|
+
|
|
95
|
+
.s-page-header[data-actions-layout='responsive'] .s-page-header__actions,
|
|
96
|
+
.s-section-header[data-trailing-layout='responsive'] .s-section-header__trailing,
|
|
97
|
+
.s-section-header[data-trailing-layout='responsive'] .s-section-header__actions { @apply w-full justify-start; }
|
|
98
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<header
|
|
3
|
+
:class="kind === 'page' ? 's-page-header' : 's-section-header'"
|
|
4
|
+
:data-kind="kind"
|
|
5
|
+
:data-actions-layout="kind === 'page' ? trailingLayout : undefined"
|
|
6
|
+
:data-trailing-layout="kind === 'section' ? trailingLayout : undefined"
|
|
7
|
+
:data-appearance="kind === 'page' ? appearance : undefined"
|
|
8
|
+
:data-size="size"
|
|
9
|
+
:data-variant="kind === 'section' ? variant : undefined"
|
|
10
|
+
:data-divider="kind === 'section' && divider ? 'true' : undefined"
|
|
11
|
+
:data-padding="kind === 'section' ? padding : undefined"
|
|
12
|
+
:data-spacing-after="kind === 'section' ? spacingAfter : undefined"
|
|
13
|
+
>
|
|
14
|
+
<div :class="kind === 'page' ? 's-page-header__copy' : 's-section-header__copy'">
|
|
15
|
+
<div
|
|
16
|
+
v-if="eyebrow"
|
|
17
|
+
:class="kind === 'page' ? 's-page-header__eyebrow' : 's-section-header__eyebrow'"
|
|
18
|
+
>{{ eyebrow }}</div>
|
|
19
|
+
<component
|
|
20
|
+
:is="`h${headingLevel}`"
|
|
21
|
+
:class="kind === 'page' ? 's-page-header__title' : 's-section-header__title'"
|
|
22
|
+
:data-presentation="titlePresentation"
|
|
23
|
+
>{{ title }}</component>
|
|
24
|
+
<p
|
|
25
|
+
v-if="description"
|
|
26
|
+
:class="kind === 'page' ? 's-page-header__description' : 's-section-header__description'"
|
|
27
|
+
>{{ description }}</p>
|
|
28
|
+
<p v-if="kind === 'section' && meta" class="s-section-header__meta">{{ meta }}</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div v-if="hasTrailing" :class="kind === 'page' ? 's-page-header__actions' : 's-section-header__trailing'">
|
|
32
|
+
<template v-if="kind === 'page'">
|
|
33
|
+
<slot name="actions" />
|
|
34
|
+
</template>
|
|
35
|
+
<template v-else>
|
|
36
|
+
<div v-if="$slots.badges" class="s-section-header__badges"><slot name="badges" /></div>
|
|
37
|
+
<div v-if="$slots.actions" class="s-section-header__actions"><slot name="actions" /></div>
|
|
38
|
+
</template>
|
|
39
|
+
</div>
|
|
40
|
+
</header>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
import { computed, useSlots } from 'vue'
|
|
45
|
+
|
|
46
|
+
export interface Props {
|
|
47
|
+
kind: 'page' | 'section'
|
|
48
|
+
title: string
|
|
49
|
+
eyebrow?: string | null
|
|
50
|
+
description?: string | null
|
|
51
|
+
meta?: string | null
|
|
52
|
+
headingLevel: 1 | 2 | 3 | 4
|
|
53
|
+
titlePresentation?: 'visible' | 'assistive'
|
|
54
|
+
appearance?: 'bare' | 'plain' | 'panel'
|
|
55
|
+
size: 'compact' | 'default' | 'sm' | 'md'
|
|
56
|
+
variant?: 'default' | 'display'
|
|
57
|
+
divider?: boolean
|
|
58
|
+
padding?: 'none' | 'sm' | 'md'
|
|
59
|
+
spacingAfter?: 'none' | 'sm' | 'md'
|
|
60
|
+
trailingLayout: 'responsive' | 'inline' | 'stacked'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
64
|
+
eyebrow: undefined,
|
|
65
|
+
description: undefined,
|
|
66
|
+
meta: undefined,
|
|
67
|
+
titlePresentation: 'visible',
|
|
68
|
+
appearance: 'panel',
|
|
69
|
+
variant: 'default',
|
|
70
|
+
divider: false,
|
|
71
|
+
padding: 'none',
|
|
72
|
+
spacingAfter: 'none',
|
|
73
|
+
})
|
|
74
|
+
const slots = useSlots()
|
|
75
|
+
const hasTrailing = computed(() => props.kind === 'page'
|
|
76
|
+
? Boolean(slots.actions)
|
|
77
|
+
: Boolean(slots.badges || slots.actions))
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<style lang="postcss" src="./SHeaderSurface.css" scoped></style>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import type { ComponentPublicInstance, ComputedRef, Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
selectionKeyToken,
|
|
6
|
+
useSelectionRoving,
|
|
7
|
+
} from './useSelectionRoving'
|
|
8
|
+
import type {
|
|
9
|
+
SelectionKey,
|
|
10
|
+
SelectionRovingOption,
|
|
11
|
+
} from './useSelectionRoving'
|
|
12
|
+
|
|
13
|
+
/** Опция общего radio-group runtime. / An option accepted by the shared radio-group runtime. */
|
|
14
|
+
export type RadioGroupOption<T extends SelectionKey> = SelectionRovingOption<T>
|
|
15
|
+
|
|
16
|
+
/** ARIA-контракт контейнера radio-group. / ARIA contract for a radio-group container. */
|
|
17
|
+
export interface RadioGroupAriaContract {
|
|
18
|
+
labelledBy: string
|
|
19
|
+
describedBy?: string
|
|
20
|
+
required?: boolean
|
|
21
|
+
invalid?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Общий runtime-контракт radio-group без визуального представления. / Shared radio-group runtime without presentation ownership. */
|
|
25
|
+
export interface RadioGroupRuntime<T extends SelectionKey> {
|
|
26
|
+
focusableValue: ComputedRef<T | undefined>
|
|
27
|
+
groupBindings: (contract: RadioGroupAriaContract) => Record<string, unknown>
|
|
28
|
+
optionBindings: (option: RadioGroupOption<T>) => Record<string, unknown>
|
|
29
|
+
optionRef: (value: T) => (element: Element | ComponentPublicInstance | null) => void
|
|
30
|
+
optionToken: (value: T) => string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveNativeElement(
|
|
34
|
+
element: Element | ComponentPublicInstance | null,
|
|
35
|
+
): HTMLElement | null {
|
|
36
|
+
return element instanceof HTMLElement ? element : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Централизует identity, ARIA, readonly/disabled и roving-focus radio-group.
|
|
41
|
+
* Centralizes identity, ARIA, readonly/disabled, and roving focus for a radio group.
|
|
42
|
+
*/
|
|
43
|
+
export function useRadioGroup<T extends SelectionKey>(config: {
|
|
44
|
+
component: string
|
|
45
|
+
options: ComputedRef<readonly RadioGroupOption<T>[]>
|
|
46
|
+
selectedValue: Ref<T | null | undefined>
|
|
47
|
+
disabled: ComputedRef<boolean>
|
|
48
|
+
readonly: ComputedRef<boolean>
|
|
49
|
+
update: (value: T) => void
|
|
50
|
+
orientation?: 'horizontal' | 'vertical' | 'both'
|
|
51
|
+
}): RadioGroupRuntime<T> {
|
|
52
|
+
const rovingOptions = computed<readonly RadioGroupOption<T>[]>(() => (
|
|
53
|
+
config.disabled.value
|
|
54
|
+
? config.options.value.map((option) => ({ ...option, disabled: true }))
|
|
55
|
+
: config.options.value
|
|
56
|
+
))
|
|
57
|
+
const select = (value: T): void => {
|
|
58
|
+
const option = config.options.value.find((candidate) => candidate.value === value)
|
|
59
|
+
if (config.disabled.value || config.readonly.value || option?.disabled) return
|
|
60
|
+
config.update(value)
|
|
61
|
+
}
|
|
62
|
+
const roving = useSelectionRoving({
|
|
63
|
+
options: rovingOptions,
|
|
64
|
+
selectedValue: config.selectedValue,
|
|
65
|
+
orientation: config.orientation,
|
|
66
|
+
select,
|
|
67
|
+
component: config.component,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const groupBindings = (contract: RadioGroupAriaContract): Record<string, unknown> => ({
|
|
71
|
+
role: 'radiogroup',
|
|
72
|
+
'aria-labelledby': contract.labelledBy,
|
|
73
|
+
'aria-describedby': contract.describedBy,
|
|
74
|
+
'aria-required': contract.required || undefined,
|
|
75
|
+
'aria-invalid': contract.invalid || undefined,
|
|
76
|
+
'aria-disabled': config.disabled.value || undefined,
|
|
77
|
+
'aria-readonly': config.readonly.value || undefined,
|
|
78
|
+
})
|
|
79
|
+
const optionBindings = (option: RadioGroupOption<T>): Record<string, unknown> => ({
|
|
80
|
+
type: 'button',
|
|
81
|
+
role: 'radio',
|
|
82
|
+
disabled: config.disabled.value || option.disabled,
|
|
83
|
+
'aria-checked': option.value === config.selectedValue.value,
|
|
84
|
+
tabindex: option.value === roving.focusableValue.value ? 0 : -1,
|
|
85
|
+
onClick: () => select(option.value),
|
|
86
|
+
onKeydown: (event: KeyboardEvent) => roving.handleKeydown(event, option.value),
|
|
87
|
+
})
|
|
88
|
+
const optionRef = (value: T) => (
|
|
89
|
+
element: Element | ComponentPublicInstance | null,
|
|
90
|
+
): void => {
|
|
91
|
+
roving.register(value, resolveNativeElement(element))
|
|
92
|
+
}
|
|
93
|
+
const optionToken = (value: T): string => selectionKeyToken(
|
|
94
|
+
config.component,
|
|
95
|
+
value,
|
|
96
|
+
'option.value',
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
focusableValue: roving.focusableValue,
|
|
101
|
+
groupBindings,
|
|
102
|
+
optionBindings,
|
|
103
|
+
optionRef,
|
|
104
|
+
optionToken,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
3
|
+
import SHeaderSurface from '../_internal/SHeaderSurface.vue'
|
|
3
4
|
|
|
4
5
|
defineOptions({ inheritAttrs: false })
|
|
5
6
|
|
|
@@ -26,26 +27,18 @@ const ownedAttrs = useOwnedAttrs({ component: 'SPageHeader', owner: 'page header
|
|
|
26
27
|
</script>
|
|
27
28
|
|
|
28
29
|
<template>
|
|
29
|
-
<
|
|
30
|
+
<SHeaderSurface
|
|
30
31
|
v-bind="ownedAttrs.bindings()"
|
|
31
|
-
|
|
32
|
-
:
|
|
33
|
-
:
|
|
34
|
-
:
|
|
32
|
+
kind="page"
|
|
33
|
+
:title="title"
|
|
34
|
+
:description="description"
|
|
35
|
+
:eyebrow="eyebrow"
|
|
36
|
+
:heading-level="headingLevel"
|
|
37
|
+
:appearance="appearance"
|
|
38
|
+
:size="size"
|
|
39
|
+
:trailing-layout="actionsLayout"
|
|
40
|
+
:title-presentation="titlePresentation"
|
|
35
41
|
>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
<component
|
|
39
|
-
:is="`h${headingLevel}`"
|
|
40
|
-
class="s-page-header__title"
|
|
41
|
-
:data-presentation="titlePresentation"
|
|
42
|
-
>{{ title }}</component>
|
|
43
|
-
<p v-if="description" class="s-page-header__description">{{ description }}</p>
|
|
44
|
-
</div>
|
|
45
|
-
<div v-if="$slots.actions" class="s-page-header__actions">
|
|
46
|
-
<slot name="actions" />
|
|
47
|
-
</div>
|
|
48
|
-
</header>
|
|
42
|
+
<template v-if="$slots.actions" #actions><slot name="actions" /></template>
|
|
43
|
+
</SHeaderSurface>
|
|
49
44
|
</template>
|
|
50
|
-
|
|
51
|
-
<style lang="postcss" src="./SPageHeader.css" scoped></style>
|
|
@@ -43,11 +43,10 @@
|
|
|
43
43
|
</template>
|
|
44
44
|
|
|
45
45
|
<script setup lang="ts">
|
|
46
|
-
import { computed, onMounted, ref, useId, watch } from 'vue'
|
|
47
46
|
import { Check, Minus } from '../../icons/sputnigUiIcons'
|
|
48
47
|
import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
49
48
|
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
50
|
-
import {
|
|
49
|
+
import { useBinaryInput } from '../../../internal/useBinaryInput'
|
|
51
50
|
import SField from './SField.vue'
|
|
52
51
|
|
|
53
52
|
defineOptions({ inheritAttrs: false })
|
|
@@ -83,48 +82,20 @@ const emit = defineEmits<{
|
|
|
83
82
|
}>()
|
|
84
83
|
const ownedAttrs = useOwnedAttrs({ component: 'SCheckbox', owner: 'native checkbox input' })
|
|
85
84
|
useInteractiveLeafRegistration({ owner: 'SCheckbox' })
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
return props.modelValue
|
|
85
|
+
const {
|
|
86
|
+
checkedValue,
|
|
87
|
+
inputEl,
|
|
88
|
+
inputId,
|
|
89
|
+
onChange,
|
|
90
|
+
onClick,
|
|
91
|
+
} = useBinaryInput({
|
|
92
|
+
owner: 'SCheckbox',
|
|
93
|
+
inputName: 'checkbox',
|
|
94
|
+
props,
|
|
95
|
+
indeterminate: () => props.indeterminate,
|
|
96
|
+
emitUpdate: (value) => emit('update:modelValue', value),
|
|
97
|
+
emitClick: (event) => emit('click', event),
|
|
101
98
|
})
|
|
102
|
-
|
|
103
|
-
function applyIndeterminate(): void {
|
|
104
|
-
if (inputEl.value) {
|
|
105
|
-
inputEl.value.indeterminate = props.indeterminate && !checkedValue.value
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
onMounted(applyIndeterminate)
|
|
110
|
-
watch([() => props.indeterminate, checkedValue], applyIndeterminate)
|
|
111
|
-
|
|
112
|
-
function onChange(event: Event): void {
|
|
113
|
-
if (props.readonly) return
|
|
114
|
-
const target = event.currentTarget
|
|
115
|
-
if (!(target instanceof HTMLInputElement)) {
|
|
116
|
-
throw new TypeError('SCheckbox: change event owner must be the native checkbox input')
|
|
117
|
-
}
|
|
118
|
-
emit('update:modelValue', target.checked)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function onClick(event: MouseEvent): void {
|
|
122
|
-
if (props.readonly) {
|
|
123
|
-
event.preventDefault()
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
emit('click', event)
|
|
127
|
-
}
|
|
128
99
|
</script>
|
|
129
100
|
|
|
130
101
|
<style lang="postcss" src="./SCheckbox.css" scoped></style>
|
|
@@ -16,26 +16,19 @@
|
|
|
16
16
|
:data-columns="columns"
|
|
17
17
|
:data-density="density"
|
|
18
18
|
:data-selection-indicator="selectionIndicator"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
:aria-readonly="readonly || undefined"
|
|
19
|
+
v-bind="radioGroup.groupBindings({
|
|
20
|
+
labelledBy: labelId,
|
|
21
|
+
describedBy,
|
|
22
|
+
required,
|
|
23
|
+
invalid,
|
|
24
|
+
})"
|
|
26
25
|
>
|
|
27
26
|
<button
|
|
28
27
|
v-for="option in options"
|
|
29
|
-
:key="optionToken(option)"
|
|
30
|
-
:ref="
|
|
28
|
+
:key="radioGroup.optionToken(option.value)"
|
|
29
|
+
:ref="radioGroup.optionRef(option.value)"
|
|
31
30
|
class="s-choice-card"
|
|
32
|
-
|
|
33
|
-
:disabled="disabled || option.disabled"
|
|
34
|
-
:aria-checked="option.value === modelValue"
|
|
35
|
-
:tabindex="option.value === focusableValue ? 0 : -1"
|
|
36
|
-
role="radio"
|
|
37
|
-
@click="select(option.value)"
|
|
38
|
-
@keydown="roving.handleKeydown($event, option.value)"
|
|
31
|
+
v-bind="radioGroup.optionBindings(option)"
|
|
39
32
|
>
|
|
40
33
|
<span v-if="option.kicker" class="s-choice-card__kicker">{{ option.kicker }}</span>
|
|
41
34
|
<span class="s-choice-card__title">{{ option.title }}</span>
|
|
@@ -83,10 +76,10 @@ export interface Props<T extends PublicSelectionKey = string> {
|
|
|
83
76
|
|
|
84
77
|
<script setup lang="ts" generic="T extends SelectionKey = string">
|
|
85
78
|
import { computed } from 'vue'
|
|
86
|
-
import type {
|
|
79
|
+
import type { VNodeChild } from 'vue'
|
|
87
80
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
88
81
|
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
89
|
-
import {
|
|
82
|
+
import { useRadioGroup } from '../_internal/useRadioGroup'
|
|
90
83
|
import type { SelectionKey } from '../_internal/useSelectionRoving'
|
|
91
84
|
import SFieldGroup from './SFieldGroup.vue'
|
|
92
85
|
|
|
@@ -116,30 +109,14 @@ defineSlots<{
|
|
|
116
109
|
}>()
|
|
117
110
|
const ownedAttrs = useOwnedAttrs({ component: 'SChoiceCards', owner: 'field group' })
|
|
118
111
|
useInteractiveLeafRegistration({ owner: 'SChoiceCards' })
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
emit('update:modelValue', value)
|
|
122
|
-
}
|
|
123
|
-
const rovingOptions = computed<readonly ChoiceCardOption<T>[]>(() => (
|
|
124
|
-
props.disabled
|
|
125
|
-
? props.options.map((option) => ({ ...option, disabled: true }))
|
|
126
|
-
: props.options
|
|
127
|
-
))
|
|
128
|
-
const roving = useSelectionRoving({
|
|
129
|
-
options: rovingOptions,
|
|
112
|
+
const radioGroup = useRadioGroup({
|
|
113
|
+
options: computed(() => props.options),
|
|
130
114
|
selectedValue: computed(() => props.modelValue),
|
|
131
|
-
|
|
115
|
+
disabled: computed(() => props.disabled),
|
|
116
|
+
readonly: computed(() => props.readonly),
|
|
117
|
+
update: (value) => emit('update:modelValue', value),
|
|
132
118
|
component: 'SChoiceCards',
|
|
133
119
|
})
|
|
134
|
-
const focusableValue = roving.focusableValue
|
|
135
|
-
const optionToken = (option: ChoiceCardOption<T>): string => selectionKeyToken(
|
|
136
|
-
'SChoiceCards',
|
|
137
|
-
option.value,
|
|
138
|
-
'option.value',
|
|
139
|
-
)
|
|
140
|
-
const resolveNativeElement = (element: Element | ComponentPublicInstance | null): HTMLElement | null => (
|
|
141
|
-
element instanceof HTMLElement ? element : null
|
|
142
|
-
)
|
|
143
120
|
</script>
|
|
144
121
|
|
|
145
122
|
<style lang="postcss" src="./SChoiceCards.css" scoped></style>
|
|
@@ -53,24 +53,12 @@
|
|
|
53
53
|
@apply flex items-center;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
.s-field__required {
|
|
57
|
-
@apply ml-0.5 text-danger-600 dark:text-danger-400;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.s-field__help,
|
|
61
|
-
.s-field__error {
|
|
62
|
-
@apply mt-1 min-w-0 whitespace-normal text-xs;
|
|
63
|
-
overflow-wrap: anywhere;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
56
|
.s-field__help {
|
|
67
57
|
grid-area: help;
|
|
68
|
-
@apply text-surface-500 dark:text-surface-400;
|
|
69
58
|
}
|
|
70
59
|
|
|
71
60
|
.s-field__error {
|
|
72
61
|
grid-area: error;
|
|
73
|
-
@apply text-danger-600 dark:text-danger-400;
|
|
74
62
|
}
|
|
75
63
|
|
|
76
64
|
.s-field[data-disabled='true'] .s-field__label,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
>
|
|
11
11
|
<label v-if="hasLabel" :id="labelId" class="s-field__label" :for="controlId">
|
|
12
12
|
<slot name="label">{{ label }}</slot>
|
|
13
|
-
<span v-if="required" class="s-field__required" aria-hidden="true">*</span>
|
|
13
|
+
<span v-if="required" :class="['s-field__required', FIELD_REQUIRED_CLASS]" aria-hidden="true">*</span>
|
|
14
14
|
</label>
|
|
15
15
|
|
|
16
16
|
<div class="s-field__control">
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
/>
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
|
-
<p v-if="helpText || $slots.help" :id="helpId" class="s-field__help">
|
|
28
|
+
<p v-if="helpText || $slots.help" :id="helpId" :class="['s-field__help', FIELD_HELP_CLASS]">
|
|
29
29
|
<slot name="help">{{ helpText }}</slot>
|
|
30
30
|
</p>
|
|
31
|
-
<p v-if="isInvalid && (errorMessage || $slots.error)" :id="errorId" class="s-field__error">
|
|
31
|
+
<p v-if="isInvalid && (errorMessage || $slots.error)" :id="errorId" :class="['s-field__error', FIELD_ERROR_CLASS]">
|
|
32
32
|
<slot name="error">{{ errorMessage }}</slot>
|
|
33
33
|
</p>
|
|
34
34
|
</div>
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
|
|
37
37
|
<script setup lang="ts">
|
|
38
38
|
import { computed, useSlots } from 'vue'
|
|
39
|
+
import {
|
|
40
|
+
FIELD_ERROR_CLASS,
|
|
41
|
+
FIELD_HELP_CLASS,
|
|
42
|
+
FIELD_REQUIRED_CLASS,
|
|
43
|
+
useFieldSupportingText,
|
|
44
|
+
} from '../../../internal/fieldPresentation'
|
|
39
45
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
40
46
|
|
|
41
47
|
defineOptions({ inheritAttrs: false })
|
|
@@ -76,13 +82,11 @@ const ownedAttrs = useOwnedAttrs({ component: 'SField', owner: 'field root' })
|
|
|
76
82
|
const isInvalid = computed(() => props.invalid)
|
|
77
83
|
const hasLabel = computed(() => Boolean(props.label || slots.label))
|
|
78
84
|
const labelId = computed(() => `${props.controlId}-label`)
|
|
79
|
-
const
|
|
80
|
-
const errorId =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (isInvalid.value && (props.errorMessage || slots.error)) ids.push(errorId.value)
|
|
85
|
-
return ids.length > 0 ? ids.join(' ') : undefined
|
|
85
|
+
const fieldId = computed(() => props.controlId)
|
|
86
|
+
const { describedBy, errorId, helpId } = useFieldSupportingText({
|
|
87
|
+
baseId: fieldId,
|
|
88
|
+
hasHelp: () => Boolean(props.helpText || slots.help),
|
|
89
|
+
hasError: () => Boolean(isInvalid.value && (props.errorMessage || slots.error)),
|
|
86
90
|
})
|
|
87
91
|
</script>
|
|
88
92
|
|
|
@@ -8,10 +8,6 @@
|
|
|
8
8
|
@apply mb-2 min-w-0 text-sm font-medium text-surface-700 dark:text-surface-300;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
.s-field-group__required {
|
|
12
|
-
@apply ml-0.5 text-danger-600 dark:text-danger-400;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
11
|
.s-field-group__controls {
|
|
16
12
|
@apply grid min-w-0 gap-2;
|
|
17
13
|
}
|
|
@@ -32,23 +28,9 @@
|
|
|
32
28
|
@apply grid-cols-4;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
.s-field-group__help,
|
|
36
|
-
.s-field-group__error {
|
|
37
|
-
@apply mt-1 min-w-0 whitespace-normal text-xs;
|
|
38
|
-
overflow-wrap: anywhere;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.s-field-group__help {
|
|
42
|
-
@apply text-surface-500 dark:text-surface-400;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.s-field-group__error {
|
|
46
|
-
@apply text-danger-600 dark:text-danger-400;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
31
|
.s-field-group[data-disabled='true'] .s-field-group__legend,
|
|
50
32
|
.s-field-group[data-readonly='true'] .s-field-group__legend {
|
|
51
|
-
|
|
33
|
+
color: var(--s-field-disabled-foreground);
|
|
52
34
|
}
|
|
53
35
|
|
|
54
36
|
@media (max-width: 40rem) {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
>
|
|
15
15
|
<legend :id="labelId" class="s-field-group__legend">
|
|
16
16
|
<slot name="label">{{ validatedLabel }}</slot>
|
|
17
|
-
<span v-if="required" class="s-field-group__required" aria-hidden="true">*</span>
|
|
17
|
+
<span v-if="required" :class="['s-field-group__required', FIELD_REQUIRED_CLASS]" aria-hidden="true">*</span>
|
|
18
18
|
</legend>
|
|
19
19
|
|
|
20
20
|
<div class="s-field-group__controls">
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
/>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
|
-
<p v-if="helpText || $slots.help" :id="helpId" class="s-field-group__help">
|
|
32
|
+
<p v-if="helpText || $slots.help" :id="helpId" :class="['s-field-group__help', FIELD_HELP_CLASS]">
|
|
33
33
|
<slot name="help">{{ helpText }}</slot>
|
|
34
34
|
</p>
|
|
35
|
-
<p v-if="invalid && (errorMessage || $slots.error)" :id="errorId" class="s-field-group__error">
|
|
35
|
+
<p v-if="invalid && (errorMessage || $slots.error)" :id="errorId" :class="['s-field-group__error', FIELD_ERROR_CLASS]">
|
|
36
36
|
<slot name="error">{{ errorMessage }}</slot>
|
|
37
37
|
</p>
|
|
38
38
|
</fieldset>
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
|
|
41
41
|
<script setup lang="ts">
|
|
42
42
|
import { computed, useId, useSlots } from 'vue'
|
|
43
|
+
import {
|
|
44
|
+
FIELD_ERROR_CLASS,
|
|
45
|
+
FIELD_HELP_CLASS,
|
|
46
|
+
FIELD_REQUIRED_CLASS,
|
|
47
|
+
useFieldSupportingText,
|
|
48
|
+
} from '../../../internal/fieldPresentation'
|
|
43
49
|
import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
44
50
|
import {
|
|
45
51
|
validateBoundedInteger,
|
|
@@ -92,13 +98,10 @@ const groupId = computed(() => validateDomId(
|
|
|
92
98
|
))
|
|
93
99
|
const labelId = computed(() => `${groupId.value}-legend`)
|
|
94
100
|
const validatedLabel = computed(() => validateNonEmptyString('SFieldGroup', 'label', props.label))
|
|
95
|
-
const helpId =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (props.helpText || slots.help) ids.push(helpId.value)
|
|
100
|
-
if (props.invalid && (props.errorMessage || slots.error)) ids.push(errorId.value)
|
|
101
|
-
return ids.length > 0 ? ids.join(' ') : undefined
|
|
101
|
+
const { describedBy, errorId, helpId } = useFieldSupportingText({
|
|
102
|
+
baseId: groupId,
|
|
103
|
+
hasHelp: () => Boolean(props.helpText || slots.help),
|
|
104
|
+
hasError: () => Boolean(props.invalid && (props.errorMessage || slots.error)),
|
|
102
105
|
})
|
|
103
106
|
const validatedLayout = computed<SFieldGroupLayout>(() => validateExactString(
|
|
104
107
|
'SFieldGroup',
|