@duro-app/ui 0.24.0 → 0.26.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/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts +44 -0
- package/dist/components/Combobox/Combobox.d.ts.map +1 -0
- package/dist/components/Combobox/Combobox.meta.d.ts +3 -0
- package/dist/components/Combobox/Combobox.meta.d.ts.map +1 -0
- package/dist/components/Combobox/Combobox.stories.d.ts +9 -0
- package/dist/components/Combobox/Combobox.stories.d.ts.map +1 -0
- package/dist/components/Combobox/ComboboxContext.d.ts +19 -0
- package/dist/components/Combobox/ComboboxContext.d.ts.map +1 -0
- package/dist/components/Combobox/styles.css.d.ts +112 -0
- package/dist/components/Combobox/styles.css.d.ts.map +1 -0
- package/dist/components/Combobox/useComboboxRoot.d.ts +15 -0
- package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -0
- package/dist/components/Field/Field.d.ts +4 -0
- package/dist/components/Field/Field.d.ts.map +1 -1
- package/dist/components/Field/Field.stories.d.ts +3 -0
- package/dist/components/Field/Field.stories.d.ts.map +1 -1
- package/dist/components/Field/FieldContext.d.ts +5 -0
- package/dist/components/Field/FieldContext.d.ts.map +1 -1
- package/dist/components/Field/styles.css.d.ts +26 -0
- package/dist/components/Field/styles.css.d.ts.map +1 -1
- package/dist/components/Form/Form.d.ts +7 -2
- package/dist/components/Form/Form.d.ts.map +1 -1
- package/dist/components/Form/Form.stories.d.ts +6 -0
- package/dist/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/components/Form/FormContext.d.ts +9 -2
- package/dist/components/Form/FormContext.d.ts.map +1 -1
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -1
- package/dist/components/Table/SortIndicator.d.ts.map +1 -1
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/useDataTable.d.ts.map +1 -1
- package/dist/components/Tag/Tag.d.ts +18 -0
- package/dist/components/Tag/Tag.d.ts.map +1 -0
- package/dist/components/Tag/Tag.meta.d.ts +3 -0
- package/dist/components/Tag/Tag.meta.d.ts.map +1 -0
- package/dist/components/Tag/Tag.stories.d.ts +11 -0
- package/dist/components/Tag/Tag.stories.d.ts.map +1 -0
- package/dist/components/Tag/styles.css.d.ts +80 -0
- package/dist/components/Tag/styles.css.d.ts.map +1 -0
- package/dist/components/TagGroup/TagGroup.d.ts +30 -0
- package/dist/components/TagGroup/TagGroup.d.ts.map +1 -0
- package/dist/components/TagGroup/TagGroup.meta.d.ts +3 -0
- package/dist/components/TagGroup/TagGroup.meta.d.ts.map +1 -0
- package/dist/components/TagGroup/TagGroup.stories.d.ts +11 -0
- package/dist/components/TagGroup/TagGroup.stories.d.ts.map +1 -0
- package/dist/components/TagGroup/TagGroupContext.d.ts +17 -0
- package/dist/components/TagGroup/TagGroupContext.d.ts.map +1 -0
- package/dist/components/TagGroup/styles.css.d.ts +75 -0
- package/dist/components/TagGroup/styles.css.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3256 -2318
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ButtonGroup/ButtonGroup.meta.ts +10 -2
- package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +4 -1
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +2 -9
- package/src/components/Combobox/Combobox.meta.ts +41 -0
- package/src/components/Combobox/Combobox.stories.tsx +157 -0
- package/src/components/Combobox/Combobox.tsx +235 -0
- package/src/components/Combobox/ComboboxContext.ts +26 -0
- package/src/components/Combobox/styles.css.ts +119 -0
- package/src/components/Combobox/useComboboxRoot.ts +235 -0
- package/src/components/Field/Field.stories.tsx +62 -0
- package/src/components/Field/Field.tsx +94 -9
- package/src/components/Field/FieldContext.ts +5 -0
- package/src/components/Field/styles.css.ts +26 -0
- package/src/components/Form/Form.stories.tsx +275 -0
- package/src/components/Form/Form.tsx +69 -7
- package/src/components/Form/FormContext.ts +10 -1
- package/src/components/Input/Input.tsx +2 -1
- package/src/components/RadioGroup/RadioGroup.meta.ts +4 -1
- package/src/components/RadioGroup/RadioGroup.tsx +1 -4
- package/src/components/Table/SortIndicator.tsx +1 -4
- package/src/components/Table/Table.stories.tsx +190 -84
- package/src/components/Table/Table.tsx +10 -5
- package/src/components/Table/useDataTable.ts +34 -48
- package/src/components/Tag/Tag.meta.ts +22 -0
- package/src/components/Tag/Tag.stories.tsx +92 -0
- package/src/components/Tag/Tag.tsx +139 -0
- package/src/components/Tag/styles.css.ts +99 -0
- package/src/components/TagGroup/TagGroup.meta.ts +27 -0
- package/src/components/TagGroup/TagGroup.stories.tsx +193 -0
- package/src/components/TagGroup/TagGroup.tsx +302 -0
- package/src/components/TagGroup/TagGroupContext.ts +22 -0
- package/src/components/TagGroup/styles.css.ts +97 -0
- package/src/components/Textarea/Textarea.tsx +2 -1
- package/src/index.ts +4 -0
- package/src/strict.css +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {type ReactNode, useCallback, useRef} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {useTagGroup} from '../TagGroup/TagGroupContext'
|
|
4
|
+
import {styles} from './styles.css'
|
|
5
|
+
|
|
6
|
+
export type TagVariant = 'default' | 'success' | 'warning' | 'error' | 'info'
|
|
7
|
+
export type TagSize = 'sm' | 'md'
|
|
8
|
+
|
|
9
|
+
interface TagProps {
|
|
10
|
+
/** Value identifier — required when used inside TagGroup */
|
|
11
|
+
value?: string
|
|
12
|
+
variant?: TagVariant
|
|
13
|
+
size?: TagSize
|
|
14
|
+
/** Show remove button. Auto-derived from TagGroup context when inside one. */
|
|
15
|
+
removable?: boolean
|
|
16
|
+
/** Called when remove button is clicked. Auto-wired to TagGroup when inside one. */
|
|
17
|
+
onRemove?: () => void
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
children: ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sizeMap = {
|
|
23
|
+
sm: styles.sizeSm,
|
|
24
|
+
md: styles.sizeMd,
|
|
25
|
+
} as const
|
|
26
|
+
|
|
27
|
+
const removableSizeMap = {
|
|
28
|
+
sm: styles.removableSm,
|
|
29
|
+
md: styles.removableMd,
|
|
30
|
+
} as const
|
|
31
|
+
|
|
32
|
+
export function Tag({
|
|
33
|
+
value,
|
|
34
|
+
variant = 'default',
|
|
35
|
+
size = 'md',
|
|
36
|
+
removable: removableProp,
|
|
37
|
+
onRemove: onRemoveProp,
|
|
38
|
+
disabled: disabledProp = false,
|
|
39
|
+
children,
|
|
40
|
+
}: TagProps) {
|
|
41
|
+
const group = useTagGroup()
|
|
42
|
+
const contentRef = useRef<HTMLElement>(null)
|
|
43
|
+
|
|
44
|
+
const isGrouped = group !== null
|
|
45
|
+
const disabled = disabledProp || (group?.disabled ?? false)
|
|
46
|
+
const removable = removableProp ?? (isGrouped ? group.editable : false)
|
|
47
|
+
|
|
48
|
+
const onRemove = useCallback(() => {
|
|
49
|
+
if (disabled) return
|
|
50
|
+
if (onRemoveProp) {
|
|
51
|
+
onRemoveProp()
|
|
52
|
+
} else if (isGrouped && value !== undefined) {
|
|
53
|
+
group.removeTag(value)
|
|
54
|
+
}
|
|
55
|
+
}, [disabled, onRemoveProp, isGrouped, value, group])
|
|
56
|
+
|
|
57
|
+
const handleKeyDown = useCallback(
|
|
58
|
+
(e: React.KeyboardEvent) => {
|
|
59
|
+
if (disabled) return
|
|
60
|
+
if ((e.key === 'Backspace' || e.key === 'Delete') && removable) {
|
|
61
|
+
e.preventDefault()
|
|
62
|
+
onRemove()
|
|
63
|
+
}
|
|
64
|
+
// Arrow key navigation delegated to TagGroup via context
|
|
65
|
+
if (isGrouped && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
const idx = value !== undefined ? group.values.indexOf(value) : -1
|
|
68
|
+
if (e.key === 'ArrowLeft' && idx > 0) {
|
|
69
|
+
group.setFocusedIndex(idx - 1)
|
|
70
|
+
} else if (e.key === 'ArrowRight') {
|
|
71
|
+
if (idx < group.values.length - 1) {
|
|
72
|
+
group.setFocusedIndex(idx + 1)
|
|
73
|
+
} else {
|
|
74
|
+
// Move focus to input
|
|
75
|
+
group.inputRef.current?.focus()
|
|
76
|
+
group.setFocusedIndex(-1)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[disabled, removable, onRemove, isGrouped, value, group],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const removeLabel = contentRef.current?.textContent
|
|
85
|
+
? `Remove ${contentRef.current.textContent}`
|
|
86
|
+
: 'Remove'
|
|
87
|
+
|
|
88
|
+
const tagContent = (
|
|
89
|
+
<>
|
|
90
|
+
<html.span ref={contentRef} {...(isGrouped ? {role: 'gridcell' as any} : {})}>
|
|
91
|
+
{children}
|
|
92
|
+
</html.span>
|
|
93
|
+
{removable && (
|
|
94
|
+
<html.button
|
|
95
|
+
type="button"
|
|
96
|
+
aria-label={removeLabel}
|
|
97
|
+
disabled={disabled}
|
|
98
|
+
onClick={(e) => {
|
|
99
|
+
;(e as any).stopPropagation?.()
|
|
100
|
+
onRemove()
|
|
101
|
+
}}
|
|
102
|
+
tabIndex={-1}
|
|
103
|
+
style={[styles.removeButton, disabled && styles.removeButtonDisabled]}
|
|
104
|
+
>
|
|
105
|
+
<svg
|
|
106
|
+
width={size === 'sm' ? 10 : 12}
|
|
107
|
+
height={size === 'sm' ? 10 : 12}
|
|
108
|
+
viewBox="0 0 12 12"
|
|
109
|
+
fill="none"
|
|
110
|
+
>
|
|
111
|
+
<path
|
|
112
|
+
d="M3 3l6 6M9 3l-6 6"
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
strokeWidth={1.5}
|
|
115
|
+
strokeLinecap="round"
|
|
116
|
+
/>
|
|
117
|
+
</svg>
|
|
118
|
+
</html.button>
|
|
119
|
+
)}
|
|
120
|
+
</>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<html.span
|
|
125
|
+
{...(isGrouped ? {role: 'row' as any} : {})}
|
|
126
|
+
tabIndex={isGrouped ? 0 : undefined}
|
|
127
|
+
onKeyDown={isGrouped ? (handleKeyDown as any) : undefined}
|
|
128
|
+
style={[
|
|
129
|
+
styles.base,
|
|
130
|
+
sizeMap[size],
|
|
131
|
+
removable && removableSizeMap[size],
|
|
132
|
+
styles[variant],
|
|
133
|
+
disabled && styles.disabled,
|
|
134
|
+
]}
|
|
135
|
+
>
|
|
136
|
+
{tagContent}
|
|
137
|
+
</html.span>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
|
|
6
|
+
export const styles = css.create({
|
|
7
|
+
base: {
|
|
8
|
+
display: 'inline-flex',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
gap: spacing.xs,
|
|
11
|
+
fontFamily: typography.fontFamily,
|
|
12
|
+
fontWeight: typography.fontWeightMedium,
|
|
13
|
+
lineHeight: 1,
|
|
14
|
+
borderRadius: radii.full,
|
|
15
|
+
whiteSpace: 'nowrap',
|
|
16
|
+
borderWidth: 0,
|
|
17
|
+
outlineWidth: {
|
|
18
|
+
default: 0,
|
|
19
|
+
':focus-visible': 2,
|
|
20
|
+
},
|
|
21
|
+
outlineStyle: {
|
|
22
|
+
default: 'none',
|
|
23
|
+
':focus-visible': 'solid',
|
|
24
|
+
},
|
|
25
|
+
outlineColor: {
|
|
26
|
+
default: 'transparent',
|
|
27
|
+
':focus-visible': colors.accent,
|
|
28
|
+
},
|
|
29
|
+
outlineOffset: {
|
|
30
|
+
default: 0,
|
|
31
|
+
':focus-visible': 1,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
sizeMd: {
|
|
35
|
+
paddingTop: spacing.xs,
|
|
36
|
+
paddingBottom: spacing.xs,
|
|
37
|
+
paddingLeft: spacing.sm,
|
|
38
|
+
paddingRight: spacing.sm,
|
|
39
|
+
fontSize: typography.fontSizeXs,
|
|
40
|
+
},
|
|
41
|
+
sizeSm: {
|
|
42
|
+
paddingTop: 2,
|
|
43
|
+
paddingBottom: 2,
|
|
44
|
+
paddingLeft: spacing.xs,
|
|
45
|
+
paddingRight: spacing.xs,
|
|
46
|
+
fontSize: '0.625rem',
|
|
47
|
+
},
|
|
48
|
+
// Reduce right padding when removable to keep visual balance with the button
|
|
49
|
+
removableMd: {
|
|
50
|
+
paddingRight: spacing.xs,
|
|
51
|
+
},
|
|
52
|
+
removableSm: {
|
|
53
|
+
paddingRight: 2,
|
|
54
|
+
},
|
|
55
|
+
default: {
|
|
56
|
+
backgroundColor: colors.bgCardHover,
|
|
57
|
+
color: colors.textMuted,
|
|
58
|
+
},
|
|
59
|
+
success: {
|
|
60
|
+
backgroundColor: colors.successBg,
|
|
61
|
+
color: colors.successText,
|
|
62
|
+
},
|
|
63
|
+
warning: {
|
|
64
|
+
backgroundColor: colors.warningBg,
|
|
65
|
+
color: colors.warningText,
|
|
66
|
+
},
|
|
67
|
+
error: {
|
|
68
|
+
backgroundColor: colors.errorBg,
|
|
69
|
+
color: colors.errorText,
|
|
70
|
+
},
|
|
71
|
+
info: {
|
|
72
|
+
backgroundColor: colors.infoBg,
|
|
73
|
+
color: colors.infoText,
|
|
74
|
+
},
|
|
75
|
+
disabled: {
|
|
76
|
+
opacity: 0.5,
|
|
77
|
+
cursor: 'not-allowed',
|
|
78
|
+
},
|
|
79
|
+
removeButton: {
|
|
80
|
+
display: 'inline-flex',
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
justifyContent: 'center',
|
|
83
|
+
padding: 0,
|
|
84
|
+
borderWidth: 0,
|
|
85
|
+
backgroundColor: 'transparent',
|
|
86
|
+
color: 'inherit',
|
|
87
|
+
cursor: 'pointer',
|
|
88
|
+
borderRadius: radii.full,
|
|
89
|
+
opacity: {
|
|
90
|
+
default: 0.6,
|
|
91
|
+
':hover': 1,
|
|
92
|
+
},
|
|
93
|
+
outlineWidth: 0,
|
|
94
|
+
},
|
|
95
|
+
removeButtonDisabled: {
|
|
96
|
+
cursor: 'not-allowed',
|
|
97
|
+
opacity: 0.4,
|
|
98
|
+
},
|
|
99
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Compound component for managing a collection of tags. Supports editable mode with an inline input for adding tags via Enter/comma, removal via X button or Backspace, and paste support for bulk adding.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Email address input (add/remove multiple emails)',
|
|
8
|
+
'Editable tag collections (categories, labels)',
|
|
9
|
+
'Static display of tag groups with optional removal',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Single value selection — use Select or Combobox',
|
|
13
|
+
'Static non-interactive labels — use Badge with Cluster',
|
|
14
|
+
'Toggle-based selection — use ToggleGroup',
|
|
15
|
+
],
|
|
16
|
+
relatedTo: [
|
|
17
|
+
{component: 'Tag', relationship: 'Tag is the item inside TagGroup.List'},
|
|
18
|
+
{component: 'Badge', relationship: 'Static display alternative'},
|
|
19
|
+
{component: 'Field', relationship: 'Wrap in Field.Root for label + description + error'},
|
|
20
|
+
],
|
|
21
|
+
example: `<TagGroup.Root name="emails" value={emails} onValueChange={setEmails}>
|
|
22
|
+
<TagGroup.List aria-label="Emails">
|
|
23
|
+
{emails.map(e => <Tag key={e} value={e}>{e}</Tag>)}
|
|
24
|
+
</TagGroup.List>
|
|
25
|
+
<TagGroup.Input placeholder="Add email..." />
|
|
26
|
+
</TagGroup.Root>`,
|
|
27
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {useState} from 'react'
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
3
|
+
import {expect, userEvent, within} from 'storybook/test'
|
|
4
|
+
import {css, html} from 'react-strict-dom'
|
|
5
|
+
import {TagGroup} from './TagGroup'
|
|
6
|
+
import {Tag} from '../Tag/Tag'
|
|
7
|
+
import {Field} from '../Field/Field'
|
|
8
|
+
import {Stack} from '../Stack/Stack'
|
|
9
|
+
|
|
10
|
+
const meta: Meta = {
|
|
11
|
+
title: 'Components/TagGroup',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Static display (no input)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export const StaticDisplay: Story = {
|
|
22
|
+
render: () => (
|
|
23
|
+
<TagGroup.Root>
|
|
24
|
+
<TagGroup.List aria-label="Technologies">
|
|
25
|
+
<Tag value="react">React</Tag>
|
|
26
|
+
<Tag value="ts">TypeScript</Tag>
|
|
27
|
+
<Tag value="node">Node.js</Tag>
|
|
28
|
+
</TagGroup.List>
|
|
29
|
+
</TagGroup.Root>
|
|
30
|
+
),
|
|
31
|
+
play: async ({canvas}) => {
|
|
32
|
+
await expect(canvas.getByText('React')).toBeInTheDocument()
|
|
33
|
+
await expect(canvas.getByText('TypeScript')).toBeInTheDocument()
|
|
34
|
+
await expect(canvas.getByText('Node.js')).toBeInTheDocument()
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Editable with input
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function EditableExample() {
|
|
43
|
+
const [tags, setTags] = useState(['react', 'typescript'])
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<TagGroup.Root value={tags} onValueChange={setTags}>
|
|
47
|
+
<TagGroup.List aria-label="Tags">
|
|
48
|
+
{tags.map((t) => (
|
|
49
|
+
<Tag key={t} value={t}>
|
|
50
|
+
{t}
|
|
51
|
+
</Tag>
|
|
52
|
+
))}
|
|
53
|
+
</TagGroup.List>
|
|
54
|
+
<TagGroup.Input placeholder="Add tag..." />
|
|
55
|
+
</TagGroup.Root>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const Editable: Story = {
|
|
60
|
+
render: () => <EditableExample />,
|
|
61
|
+
play: async ({canvas}) => {
|
|
62
|
+
await expect(canvas.getByText('react')).toBeInTheDocument()
|
|
63
|
+
await expect(canvas.getByText('typescript')).toBeInTheDocument()
|
|
64
|
+
// Input should be present
|
|
65
|
+
await expect(canvas.getByRole('textbox')).toBeInTheDocument()
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// With default value (uncontrolled)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export const WithDefaultValue: Story = {
|
|
74
|
+
render: () => {
|
|
75
|
+
function Uncontrolled() {
|
|
76
|
+
const [tags, setTags] = useState(['one', 'two'])
|
|
77
|
+
return (
|
|
78
|
+
<TagGroup.Root defaultValue={['one', 'two']} onValueChange={setTags}>
|
|
79
|
+
<TagGroup.List aria-label="Items">
|
|
80
|
+
{tags.map((t) => (
|
|
81
|
+
<Tag key={t} value={t}>
|
|
82
|
+
{t}
|
|
83
|
+
</Tag>
|
|
84
|
+
))}
|
|
85
|
+
</TagGroup.List>
|
|
86
|
+
<TagGroup.Input placeholder="Add..." />
|
|
87
|
+
</TagGroup.Root>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return <Uncontrolled />
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Email input with validation
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
function EmailExample() {
|
|
99
|
+
const [emails, setEmails] = useState<string[]>([])
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Stack gap="md">
|
|
103
|
+
<Field.Root required>
|
|
104
|
+
<Field.Label>Invite emails</Field.Label>
|
|
105
|
+
<TagGroup.Root
|
|
106
|
+
name="emails"
|
|
107
|
+
value={emails}
|
|
108
|
+
onValueChange={setEmails}
|
|
109
|
+
onValidate={(v) => (v.includes('@') ? true : 'Invalid email')}
|
|
110
|
+
>
|
|
111
|
+
<TagGroup.List aria-label="Emails">
|
|
112
|
+
{emails.map((e) => (
|
|
113
|
+
<Tag key={e} value={e}>
|
|
114
|
+
{e}
|
|
115
|
+
</Tag>
|
|
116
|
+
))}
|
|
117
|
+
</TagGroup.List>
|
|
118
|
+
<TagGroup.Input placeholder="Add email..." />
|
|
119
|
+
</TagGroup.Root>
|
|
120
|
+
<Field.Description>Press Enter or comma to add each email</Field.Description>
|
|
121
|
+
</Field.Root>
|
|
122
|
+
</Stack>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const EmailInput: Story = {
|
|
127
|
+
render: () => <EmailExample />,
|
|
128
|
+
play: async ({canvas}) => {
|
|
129
|
+
const input = canvas.getByRole('textbox')
|
|
130
|
+
await expect(input).toBeInTheDocument()
|
|
131
|
+
// Type an email and press Enter
|
|
132
|
+
await userEvent.type(input, 'alice@example.com{Enter}')
|
|
133
|
+
await expect(canvas.getByText('alice@example.com')).toBeInTheDocument()
|
|
134
|
+
// Type another
|
|
135
|
+
await userEvent.type(input, 'bob@test.com,')
|
|
136
|
+
await expect(canvas.getByText('bob@test.com')).toBeInTheDocument()
|
|
137
|
+
// Invalid email should not create a tag
|
|
138
|
+
await userEvent.type(input, 'notanemail{Enter}')
|
|
139
|
+
expect(canvas.queryByText('notanemail')).not.toBeInTheDocument()
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Disabled
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
function DisabledExample() {
|
|
148
|
+
const tags = ['react', 'vue', 'svelte']
|
|
149
|
+
return (
|
|
150
|
+
<TagGroup.Root value={tags} disabled>
|
|
151
|
+
<TagGroup.List aria-label="Frameworks">
|
|
152
|
+
{tags.map((t) => (
|
|
153
|
+
<Tag key={t} value={t}>
|
|
154
|
+
{t}
|
|
155
|
+
</Tag>
|
|
156
|
+
))}
|
|
157
|
+
</TagGroup.List>
|
|
158
|
+
<TagGroup.Input placeholder="Disabled..." />
|
|
159
|
+
</TagGroup.Root>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const Disabled: Story = {
|
|
164
|
+
render: () => <DisabledExample />,
|
|
165
|
+
play: async ({canvas}) => {
|
|
166
|
+
await expect(canvas.getByRole('textbox')).toBeDisabled()
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Tag variants inside TagGroup
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
export const WithVariants: Story = {
|
|
175
|
+
render: () => (
|
|
176
|
+
<TagGroup.Root>
|
|
177
|
+
<TagGroup.List aria-label="Status">
|
|
178
|
+
<Tag value="active" variant="success">
|
|
179
|
+
Active
|
|
180
|
+
</Tag>
|
|
181
|
+
<Tag value="pending" variant="warning">
|
|
182
|
+
Pending
|
|
183
|
+
</Tag>
|
|
184
|
+
<Tag value="expired" variant="error">
|
|
185
|
+
Expired
|
|
186
|
+
</Tag>
|
|
187
|
+
<Tag value="info" variant="info">
|
|
188
|
+
Updated
|
|
189
|
+
</Tag>
|
|
190
|
+
</TagGroup.List>
|
|
191
|
+
</TagGroup.Root>
|
|
192
|
+
),
|
|
193
|
+
}
|