@react-spectrum/tag 3.0.0-nightly.3180 → 3.0.0-rc.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/import.mjs +676 -0
- package/dist/main.css +1 -1
- package/dist/main.js +593 -191
- package/dist/main.js.map +1 -1
- package/dist/module.js +591 -175
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -20
- package/src/Tag.tsx +36 -43
- package/src/TagGroup.tsx +202 -92
- package/src/index.ts +3 -3
package/src/TagGroup.tsx
CHANGED
|
@@ -10,113 +10,223 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
13
|
+
import {ActionButton} from '@react-spectrum/button';
|
|
14
|
+
import {AriaTagGroupProps, TagKeyboardDelegate, useTagGroup} from '@react-aria/tag';
|
|
15
|
+
import {classNames, useDOMRef} from '@react-spectrum/utils';
|
|
16
|
+
import {DOMRef, SpectrumHelpTextProps, SpectrumLabelableProps, StyleProps} from '@react-types/shared';
|
|
17
|
+
import {Field} from '@react-spectrum/label';
|
|
18
|
+
import {FocusScope} from '@react-aria/focus';
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
import intlMessages from '../intl/*.json';
|
|
21
|
+
import {ListCollection} from '@react-stately/list';
|
|
22
|
+
import {Provider, useProviderProps} from '@react-spectrum/provider';
|
|
23
|
+
import React, {ReactElement, useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
19
24
|
import styles from '@adobe/spectrum-css-temp/components/tags/vars.css';
|
|
20
25
|
import {Tag} from './Tag';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {useProviderProps} from '@react-spectrum/provider';
|
|
26
|
+
import {useFormProps} from '@react-spectrum/form';
|
|
27
|
+
import {useId, useLayoutEffect, useResizeObserver, useValueEffect} from '@react-aria/utils';
|
|
28
|
+
import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';
|
|
29
|
+
import {useTagGroupState} from '@react-stately/tag';
|
|
26
30
|
|
|
31
|
+
export interface SpectrumTagGroupProps<T> extends AriaTagGroupProps<T>, StyleProps, SpectrumLabelableProps, Omit<SpectrumHelpTextProps, 'showErrorIcon'> {
|
|
32
|
+
/** The label to display on the action button. */
|
|
33
|
+
actionLabel?: string,
|
|
34
|
+
/** Handler that is called when the action button is pressed. */
|
|
35
|
+
onAction?: () => void
|
|
36
|
+
}
|
|
27
37
|
|
|
28
38
|
function TagGroup<T extends object>(props: SpectrumTagGroupProps<T>, ref: DOMRef<HTMLDivElement>) {
|
|
29
39
|
props = useProviderProps(props);
|
|
40
|
+
props = useFormProps(props);
|
|
30
41
|
let {
|
|
31
|
-
|
|
32
|
-
isRemovable,
|
|
42
|
+
allowsRemoving,
|
|
33
43
|
onRemove,
|
|
34
|
-
|
|
44
|
+
maxRows,
|
|
45
|
+
children,
|
|
46
|
+
actionLabel,
|
|
47
|
+
onAction,
|
|
48
|
+
labelPosition
|
|
35
49
|
} = props;
|
|
36
50
|
let domRef = useDOMRef(ref);
|
|
37
|
-
let
|
|
51
|
+
let containerRef = useRef(null);
|
|
52
|
+
let tagsRef = useRef(null);
|
|
38
53
|
let {direction} = useLocale();
|
|
39
|
-
let
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
55
|
+
let [isCollapsed, setIsCollapsed] = useState(maxRows != null);
|
|
56
|
+
let state = useTagGroupState(props);
|
|
57
|
+
let [tagState, setTagState] = useValueEffect({visibleTagCount: state.collection.size, showCollapseButton: false, maxHeight: undefined});
|
|
58
|
+
let keyboardDelegate = useMemo(() => (
|
|
59
|
+
isCollapsed
|
|
60
|
+
? new TagKeyboardDelegate(new ListCollection([...state.collection].slice(0, tagState.visibleTagCount)), direction)
|
|
61
|
+
: new TagKeyboardDelegate(new ListCollection([...state.collection]), direction)
|
|
62
|
+
), [direction, isCollapsed, state.collection, tagState.visibleTagCount]) as TagKeyboardDelegate<T>;
|
|
63
|
+
// Remove onAction from props so it doesn't make it into useGridList.
|
|
64
|
+
delete props.onAction;
|
|
65
|
+
let {gridProps, labelProps, descriptionProps, errorMessageProps} = useTagGroup({...props, keyboardDelegate}, state, tagsRef);
|
|
66
|
+
let actionsId = useId();
|
|
67
|
+
|
|
68
|
+
let updateVisibleTagCount = useCallback(() => {
|
|
69
|
+
if (maxRows > 0) {
|
|
70
|
+
let computeVisibleTagCount = () => {
|
|
71
|
+
// Refs can be null at runtime.
|
|
72
|
+
let currContainerRef: HTMLDivElement | null = containerRef.current;
|
|
73
|
+
let currTagsRef: HTMLDivElement | null = tagsRef.current;
|
|
74
|
+
if (!currContainerRef || !currTagsRef) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let tags = [...currTagsRef.children];
|
|
79
|
+
let buttons = [...currContainerRef.parentElement.querySelectorAll('button')];
|
|
80
|
+
let currY = -Infinity;
|
|
81
|
+
let rowCount = 0;
|
|
82
|
+
let index = 0;
|
|
83
|
+
let tagWidths: number[] = [];
|
|
84
|
+
// Count rows and show tags until we hit the maxRows.
|
|
85
|
+
for (let tag of tags) {
|
|
86
|
+
let {width, y} = tag.getBoundingClientRect();
|
|
87
|
+
|
|
88
|
+
if (y !== currY) {
|
|
89
|
+
currY = y;
|
|
90
|
+
rowCount++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (rowCount > maxRows) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
tagWidths.push(width);
|
|
97
|
+
index++;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Remove tags until there is space for the collapse button and action button (if present) on the last row.
|
|
101
|
+
let buttonsWidth = buttons.reduce((acc, curr) => acc += curr.getBoundingClientRect().width, 0);
|
|
102
|
+
buttonsWidth += parseInt(window.getComputedStyle(buttons[buttons.length - 1]).marginRight, 10) * 2;
|
|
103
|
+
let end = direction === 'ltr' ? 'right' : 'left';
|
|
104
|
+
let containerEnd = currContainerRef.parentElement.getBoundingClientRect()[end];
|
|
105
|
+
let lastTagEnd = tags[index - 1]?.getBoundingClientRect()[end];
|
|
106
|
+
lastTagEnd += parseInt(window.getComputedStyle(tags[index - 1]).marginRight, 10);
|
|
107
|
+
let availableWidth = containerEnd - lastTagEnd;
|
|
108
|
+
|
|
109
|
+
while (availableWidth < buttonsWidth && index < state.collection.size && index > 0) {
|
|
110
|
+
availableWidth += tagWidths.pop();
|
|
111
|
+
index--;
|
|
112
|
+
}
|
|
113
|
+
let tagStyle = window.getComputedStyle(tags[0]);
|
|
114
|
+
let maxHeight = (parseInt(tagStyle.height, 10) + parseInt(tagStyle.marginTop, 10) * 2) * maxRows;
|
|
115
|
+
return {
|
|
116
|
+
visibleTagCount: index,
|
|
117
|
+
showCollapseButton: index < state.collection.size,
|
|
118
|
+
maxHeight
|
|
119
|
+
};
|
|
67
120
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
|
|
122
|
+
setTagState(function *() {
|
|
123
|
+
// Update to show all items.
|
|
124
|
+
yield {visibleTagCount: state.collection.size, showCollapseButton: true, maxHeight: undefined};
|
|
125
|
+
|
|
126
|
+
// Measure, and update to show the items until maxRows is reached.
|
|
127
|
+
yield computeVisibleTagCount();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}, [maxRows, setTagState, direction, state.collection.size]);
|
|
131
|
+
|
|
132
|
+
useResizeObserver({ref: containerRef, onResize: updateVisibleTagCount});
|
|
133
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
+
useLayoutEffect(updateVisibleTagCount, [children]);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
// Recalculate visible tags when fonts are loaded.
|
|
138
|
+
document.fonts?.ready.then(() => updateVisibleTagCount());
|
|
139
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
let visibleTags = [...state.collection];
|
|
143
|
+
if (maxRows != null && isCollapsed) {
|
|
144
|
+
visibleTags = visibleTags.slice(0, tagState.visibleTagCount);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let handlePressCollapse = () => {
|
|
148
|
+
// Prevents button from losing focus if focusedKey got collapsed.
|
|
149
|
+
state.selectionManager.setFocusedKey(null);
|
|
150
|
+
setIsCollapsed(prevCollapsed => !prevCollapsed);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
let showActions = tagState.showCollapseButton || (actionLabel && onAction);
|
|
154
|
+
|
|
91
155
|
return (
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
156
|
+
<FocusScope>
|
|
157
|
+
<Field
|
|
158
|
+
{...props}
|
|
159
|
+
labelProps={labelProps}
|
|
160
|
+
descriptionProps={descriptionProps}
|
|
161
|
+
errorMessageProps={errorMessageProps}
|
|
162
|
+
showErrorIcon
|
|
163
|
+
ref={domRef}
|
|
164
|
+
elementType="span"
|
|
165
|
+
UNSAFE_className={
|
|
166
|
+
classNames(
|
|
167
|
+
styles,
|
|
168
|
+
'spectrum-Tags-fieldWrapper',
|
|
169
|
+
{
|
|
170
|
+
'spectrum-Tags-fieldWrapper--positionSide': labelPosition === 'side'
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
}>
|
|
174
|
+
<div
|
|
175
|
+
style={maxRows != null && tagState.showCollapseButton && isCollapsed ? {maxHeight: tagState.maxHeight, overflow: 'hidden'} : undefined}
|
|
176
|
+
ref={containerRef}
|
|
177
|
+
className={classNames(styles, 'spectrum-Tags-container')}>
|
|
178
|
+
<div
|
|
179
|
+
ref={tagsRef}
|
|
180
|
+
{...gridProps}
|
|
181
|
+
className={classNames(styles, 'spectrum-Tags')}>
|
|
182
|
+
{visibleTags.map(item => (
|
|
183
|
+
<Tag
|
|
184
|
+
{...item.props}
|
|
185
|
+
key={item.key}
|
|
186
|
+
item={item}
|
|
187
|
+
state={state}
|
|
188
|
+
allowsRemoving={allowsRemoving}
|
|
189
|
+
onRemove={onRemove}>
|
|
190
|
+
{item.rendered}
|
|
191
|
+
</Tag>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
{showActions &&
|
|
195
|
+
<Provider isDisabled={false}>
|
|
196
|
+
<div
|
|
197
|
+
role="group"
|
|
198
|
+
id={actionsId}
|
|
199
|
+
aria-label={stringFormatter.format('actions')}
|
|
200
|
+
aria-labelledby={`${gridProps.id} ${actionsId}`}
|
|
201
|
+
className={classNames(styles, 'spectrum-Tags-actions')}>
|
|
202
|
+
{tagState.showCollapseButton &&
|
|
203
|
+
<ActionButton
|
|
204
|
+
isQuiet
|
|
205
|
+
onPress={handlePressCollapse}
|
|
206
|
+
UNSAFE_className={classNames(styles, 'spectrum-Tags-actionButton')}>
|
|
207
|
+
{isCollapsed ?
|
|
208
|
+
stringFormatter.format('showAllButtonLabel', {tagCount: state.collection.size}) :
|
|
209
|
+
stringFormatter.format('hideButtonLabel')
|
|
210
|
+
}
|
|
211
|
+
</ActionButton>
|
|
212
|
+
}
|
|
213
|
+
{actionLabel && onAction &&
|
|
214
|
+
<ActionButton
|
|
215
|
+
isQuiet
|
|
216
|
+
onPress={() => onAction?.()}
|
|
217
|
+
UNSAFE_className={classNames(styles, 'spectrum-Tags-actionButton')}>
|
|
218
|
+
{actionLabel}
|
|
219
|
+
</ActionButton>
|
|
220
|
+
}
|
|
221
|
+
</div>
|
|
222
|
+
</Provider>
|
|
223
|
+
}
|
|
224
|
+
</div>
|
|
225
|
+
</Field>
|
|
226
|
+
</FocusScope>
|
|
118
227
|
);
|
|
119
228
|
}
|
|
120
229
|
|
|
230
|
+
/** Tags allow users to categorize content. They can represent keywords or people, and are grouped to describe an item or a search request. */
|
|
121
231
|
const _TagGroup = React.forwardRef(TagGroup) as <T>(props: SpectrumTagGroupProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
|
|
122
232
|
export {_TagGroup as TagGroup};
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export
|
|
13
|
+
export {TagGroup} from './TagGroup';
|
|
14
|
+
export {Item} from '@react-stately/collections';
|
|
15
|
+
export type {SpectrumTagGroupProps} from './TagGroup';
|