Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1a39b79
add defaultVisibleRows and initial functionality
reidbarber Nov 16, 2022
aba67e9
cleanup
reidbarber Nov 16, 2022
31eeeb1
rename to maxRows
reidbarber Nov 16, 2022
2937e02
switch to use quiet ActionButton
reidbarber Nov 16, 2022
c93a145
include collapse button on last row
reidbarber Nov 17, 2022
5b1d073
revert cleanup
reidbarber Dec 1, 2022
146e8c9
Merge branch 'main' into tagroup-show-all
reidbarber Dec 2, 2022
888562d
update logic
reidbarber Dec 2, 2022
3a97857
update to use generator
reidbarber Dec 5, 2022
ae21d32
Merge branch 'main' into tagroup-show-all
reidbarber Dec 5, 2022
4b2ecbe
include show all button on last row
reidbarber Dec 6, 2022
49b75a3
update story to fill width for resizing
reidbarber Dec 6, 2022
bba7bac
add test
reidbarber Dec 6, 2022
4639f4d
cleanup
reidbarber Dec 6, 2022
af7d23e
add FocusScope
reidbarber Dec 6, 2022
9e7bd1c
improve performance
reidbarber Dec 7, 2022
58ffb28
add function for making room for collapse button
reidbarber Dec 7, 2022
dfc95aa
fix indention
reidbarber Dec 7, 2022
ca308a3
move into one function
reidbarber Dec 7, 2022
62392e7
update test
reidbarber Dec 7, 2022
5505e2f
address review comments
reidbarber Dec 7, 2022
4d71351
fix logic for showing button
reidbarber Dec 8, 2022
692654f
fix test
reidbarber Dec 8, 2022
7941553
condense getBoundingClientRect calls
reidbarber Dec 9, 2022
fdbfc14
update test
reidbarber Dec 9, 2022
7062bd2
Merge branch 'main' into tagroup-show-all
reidbarber Dec 9, 2022
f61d815
don't hide tags if all tags shown
reidbarber Dec 9, 2022
a240431
update mock values
reidbarber Dec 12, 2022
1d941e1
Merge branch 'main' into tagroup-show-all
reidbarber Dec 19, 2022
f6204de
Merge branch 'main' into tagroup-show-all
reidbarber Dec 19, 2022
6a1f746
update to CSF3
reidbarber Dec 20, 2022
d527fd1
add test for not showing button
reidbarber Dec 20, 2022
740a6b0
Merge branch 'main' into tagroup-show-all
reidbarber Dec 20, 2022
32479a1
handle case of container width smaller than tag
reidbarber Jan 3, 2023
d21265c
Merge branch 'main' into tagroup-show-all
reidbarber Jan 4, 2023
bf793f5
update deps
reidbarber Jan 4, 2023
4e7ca75
use custom keyboardDelegate when collapsed
reidbarber Jan 5, 2023
fcafa64
lint
reidbarber Jan 5, 2023
df2409f
allow tabbing to collapse button
reidbarber Jan 5, 2023
81f1877
lint
reidbarber Jan 6, 2023
a1c5d7a
fix button losing focus if focusedKey collapsed
reidbarber Jan 6, 2023
aa360a6
add story for onRemove + maxRows
reidbarber Jan 6, 2023
bbbd110
move button outside of grid
reidbarber Jan 10, 2023
d3a0f99
lint
reidbarber Jan 10, 2023
0bb236e
Merge branch 'main' into tagroup-show-all
reidbarber Jan 10, 2023
c05009b
Merge branch 'main' into tagroup-show-all
reidbarber Jan 12, 2023
3096d2c
fix refs for overflow detection
reidbarber Jan 12, 2023
f248c6e
add translation string
reidbarber Jan 12, 2023
df30f44
remove extraneous style
reidbarber Jan 12, 2023
1c14b1b
fix actionbutton style
reidbarber Jan 12, 2023
5874296
Merge branch 'main' into tagroup-show-all
reidbarber Jan 12, 2023
e8829a8
recalculate after fonts load
reidbarber Jan 12, 2023
a4c3f5e
Merge branch 'main' into tagroup-show-all
reidbarber Jan 12, 2023
2bcfc57
Merge branch 'main' into tagroup-show-all
reidbarber Jan 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions packages/@adobe/spectrum-css-temp/components/tags/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,28 @@ governing permissions and limitations under the License.

@import '../commons/index.css';

.spectrum-Tags {
display: flex;
flex-wrap: wrap;
:root {
--spectrum-tag-margin: calc(var(--spectrum-taggroup-tag-gap-y) / 2) calc(var(--spectrum-taggroup-tag-gap-x) / 2);
}

margin: 0;
padding: 0;
list-style: none;
.spectrum-Tags {
display: inline;
}

.spectrum-Tags-item {
@inherit: %spectrum-FocusRing;
--spectrum-focus-ring-border-radius: var(--spectrum-tag-border-radius);
--spectrum-focus-ring-border-size: var(--spectrum-tag-border-size);

display: grid;
display: inline-grid;
grid-template-columns: auto 1fr auto;
grid-template-areas: "icon content action";
align-items: center;
box-sizing: border-box;
position: relative;

margin: calc(var(--spectrum-taggroup-tag-gap-y) / 2) calc(var(--spectrum-taggroup-tag-gap-x) / 2);
padding-inline-start:calc(var(--spectrum-tag-padding-x) - var(--spectrum-tag-border-size));
margin: var(--spectrum-tag-margin);
padding-inline-start: calc(var(--spectrum-tag-padding-x) - var(--spectrum-tag-border-size));
block-size: var(--spectrum-tag-height);
max-inline-size: 100%;

Expand Down Expand Up @@ -91,3 +90,15 @@ governing permissions and limitations under the License.
}
}

.spectrum-Tags-actionButton {
display: inline;
height: var(--spectrum-tag-height);
font-size: var(--spectrum-tag-text-size);
margin: var(--spectrum-tag-margin);

> span {
padding-inline-start: var(--spectrum-tag-padding-x);
padding-inline-end: var(--spectrum-tag-padding-x);
}
}

1 change: 1 addition & 0 deletions packages/@react-aria/tag/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@react-aria/i18n": "^3.6.3",
"@react-aria/interactions": "^3.13.1",
"@react-aria/utils": "^3.14.2",
"@react-stately/list": "^3.6.1",
"@react-stately/tag": "3.0.0-alpha.1",
"@react-types/button": "^3.7.0",
"@react-types/shared": "^3.16.0",
Expand Down
1 change: 1 addition & 0 deletions packages/@react-aria/tag/src/TagKeyboardDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class TagKeyboardDelegate<T> implements KeyboardDelegate {

// Find the next item
key = this.collection.getKeyAfter(key);

if (key != null) {
return key;
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/tag/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ export {useTag} from './useTag';
export {useTagGroup} from './useTagGroup';

export type {TagProps} from '@react-types/tag';
export type {TagGroupAria} from './useTagGroup';
export type {TagGroupAria, AriaTagGroupProps} from './useTagGroup';
export type {TagAria} from './useTag';
16 changes: 12 additions & 4 deletions packages/@react-aria/tag/src/useTagGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
* governing permissions and limitations under the License.
*/

import {AriaTagGroupProps} from '@react-types/tag';
import {DOMAttributes} from '@react-types/shared';
import {AriaLabelingProps, DOMAttributes, DOMProps} from '@react-types/shared';
import {filterDOMProps, mergeProps} from '@react-aria/utils';
import {RefObject, useState} from 'react';
import {TagGroupProps} from '@react-types/tag';
import type {TagGroupState} from '@react-stately/tag';
import {TagKeyboardDelegate} from './TagKeyboardDelegate';
import {useFocusWithin} from '@react-aria/interactions';
Expand All @@ -24,6 +24,14 @@ export interface TagGroupAria {
tagGroupProps: DOMAttributes
}

export interface AriaTagGroupProps<T> extends TagGroupProps<T>, DOMProps, AriaLabelingProps {
/**
* An optional keyboard delegate to handle arrow key navigation,
* to override the default.
*/
keyboardDelegate?: TagKeyboardDelegate<T>
}
Comment thread
reidbarber marked this conversation as resolved.

/**
* Provides the behavior and accessibility implementation for a tag group component.
* Tags allow users to categorize content. They can represent keywords or people, and are grouped to describe an item or a search request.
Expand All @@ -33,7 +41,7 @@ export interface TagGroupAria {
*/
export function useTagGroup<T>(props: AriaTagGroupProps<T>, state: TagGroupState<T>, ref: RefObject<HTMLElement>): TagGroupAria {
let {direction} = useLocale();
let keyboardDelegate = new TagKeyboardDelegate(state.collection, direction);
let keyboardDelegate = props.keyboardDelegate || new TagKeyboardDelegate(state.collection, direction);
let {gridProps} = useGridList({...props, keyboardDelegate}, state, ref);

// Don't want the grid to be focusable or accessible via keyboard
Expand All @@ -51,6 +59,6 @@ export function useTagGroup<T>(props: AriaTagGroupProps<T>, state: TagGroupState
'aria-relevant': 'additions',
'aria-live': isFocusWithin ? 'polite' : 'off',
...focusWithinProps
} as DOMAttributes)
})
};
}
4 changes: 4 additions & 0 deletions packages/@react-spectrum/tag/intl/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"showAllButtonLabel": "Show all ({tagCount})",
"hideButtonLabel": "Show less"
}
2 changes: 2 additions & 0 deletions packages/@react-spectrum/tag/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
},
"dependencies": {
"@react-aria/focus": "^3.10.1",
"@react-aria/i18n": "^3.6.3",
"@react-aria/interactions": "^3.13.1",
"@react-aria/tag": "3.0.0-beta.1",
"@react-aria/utils": "^3.14.2",
"@react-spectrum/button": "^3.11.2",
"@react-spectrum/text": "^3.3.4",
"@react-spectrum/utils": "^3.8.1",
"@react-stately/collections": "^3.5.1",
"@react-stately/list": "^3.6.1",
"@react-stately/tag": "3.0.0-alpha.1",
"@react-types/shared": "^3.16.0",
"@react-types/tag": "3.0.0-beta.1",
Expand Down
167 changes: 138 additions & 29 deletions packages/@react-spectrum/tag/src/TagGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,161 @@
* governing permissions and limitations under the License.
*/

import {ActionButton} from '@react-spectrum/button';
import {AriaTagGroupProps, TagKeyboardDelegate, useTagGroup} from '@react-aria/tag';
import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils';
import {DOMRef} from '@react-types/shared';
import {mergeProps} from '@react-aria/utils';
import React, {ReactElement} from 'react';
import {SpectrumTagGroupProps} from '@react-types/tag';
import {DOMRef, StyleProps} from '@react-types/shared';
import {FocusScope} from '@react-aria/focus';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {ListCollection} from '@react-stately/list';
import React, {ReactElement, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import styles from '@adobe/spectrum-css-temp/components/tags/vars.css';
import {Tag} from './Tag';
import {useLayoutEffect, useResizeObserver, useValueEffect} from '@react-aria/utils';
import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';
import {useProviderProps} from '@react-spectrum/provider';
import {useTagGroup} from '@react-aria/tag';
import {useTagGroupState} from '@react-stately/tag';

export interface SpectrumTagGroupProps<T> extends AriaTagGroupProps<T>, StyleProps {}

function TagGroup<T extends object>(props: SpectrumTagGroupProps<T>, ref: DOMRef<HTMLDivElement>) {
props = useProviderProps(props);
let {
allowsRemoving,
onRemove,
maxRows,
children,
...otherProps
} = props;
let domRef = useDOMRef(ref);
let containerRef = useRef(null);
let {styleProps} = useStyleProps(otherProps);
let {direction} = useLocale();
let stringFormatter = useLocalizedStringFormatter(intlMessages);
let [isCollapsed, setIsCollapsed] = useState(maxRows != null);
let state = useTagGroupState(props);
let {tagGroupProps} = useTagGroup(props, state, domRef);
let [tagState, setTagState] = useValueEffect({visibleTagCount: state.collection.size, showCollapseButton: false});
let keyboardDelegate = useMemo(() => (
isCollapsed
? new TagKeyboardDelegate(new ListCollection([...state.collection].slice(0, tagState.visibleTagCount)), direction)
: new TagKeyboardDelegate(new ListCollection([...state.collection]), direction)
), [direction, isCollapsed, state.collection, tagState.visibleTagCount]) as TagKeyboardDelegate<T>;
let {tagGroupProps} = useTagGroup({...props, keyboardDelegate}, state, domRef);

let updateVisibleTagCount = useCallback(() => {
if (maxRows > 0) {
let computeVisibleTagCount = () => {
// Refs can be null at runtime.
let currDomRef: HTMLDivElement | null = domRef.current;
let currContainerRef: HTMLDivElement | null = containerRef.current;
if (!currDomRef || !currContainerRef) {
return;
}

let tags = [...currDomRef.children];
let button = currContainerRef.querySelector('button');
let currY = -Infinity;
let rowCount = 0;
let index = 0;
let tagWidths = [];
// Count rows and show tags until we hit the maxRows.
for (let tag of tags) {
let {width, y} = tag.getBoundingClientRect();

if (y !== currY) {
currY = y;
rowCount++;
}

if (rowCount > maxRows) {
break;
}
tagWidths.push(width);
index++;
}

// Remove tags until there is space for the collapse button on the last row.
let buttonWidth = button.getBoundingClientRect().width;
let end = direction === 'ltr' ? 'right' : 'left';
let containerEnd = currContainerRef.getBoundingClientRect()[end];
let lastTagEnd = tags[index - 1]?.getBoundingClientRect()[end];
let availableWidth = containerEnd - lastTagEnd;
for (let tagWidth of tagWidths.reverse()) {
if (availableWidth >= buttonWidth || index <= 1 || index >= state.collection.size) {
break;
}
availableWidth += tagWidth;
index--;
}
return {visibleTagCount: index, showCollapseButton: index < state.collection.size};
};

setTagState(function *() {
// Update to show all items.
yield {visibleTagCount: state.collection.size, showCollapseButton: true};

// Measure, and update to show the items until maxRows is reached.
yield computeVisibleTagCount();
});
}
}, [maxRows, setTagState, domRef, direction, state.collection.size]);

useResizeObserver({ref: containerRef, onResize: updateVisibleTagCount});

// eslint-disable-next-line react-hooks/exhaustive-deps
useLayoutEffect(updateVisibleTagCount, [children]);

useEffect(() => {
// Recalculate visible tags when fonts are loaded.
document.fonts?.ready.then(() => updateVisibleTagCount());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

let visibleTags = [...state.collection];
if (maxRows != null && isCollapsed) {
visibleTags = visibleTags.slice(0, tagState.visibleTagCount);
}

let handlePressCollapse = () => {
// Prevents button from losing focus if focusedKey got collapsed.
state.selectionManager.setFocusedKey(null);
setIsCollapsed(prevCollapsed => !prevCollapsed);
};

return (
<div
{...mergeProps(styleProps, tagGroupProps)}
className={
classNames(
styles,
'spectrum-Tags',
styleProps.className
)
}
role={state.collection.size ? 'grid' : null}
ref={domRef}>
{[...state.collection].map(item => (
<Tag
{...item.props}
key={item.key}
item={item}
state={state}
allowsRemoving={allowsRemoving}
onRemove={onRemove}>
{item.rendered}
</Tag>
))}
</div>
<FocusScope>
<div
ref={containerRef}
{...styleProps}
className={classNames(styles, 'spectrum-Tags-container', styleProps.className)}>
<div
{...tagGroupProps}
className={classNames(styles, 'spectrum-Tags')}
role={state.collection.size ? 'grid' : null}
ref={domRef}>
{visibleTags.map(item => (
<Tag
{...item.props}
key={item.key}
item={item}
state={state}
allowsRemoving={allowsRemoving}
onRemove={onRemove}>
{item.rendered}
</Tag>
))}
</div>
{tagState.showCollapseButton &&
<ActionButton
isQuiet
onPress={handlePressCollapse}
UNSAFE_className={classNames(styles, 'spectrum-Tags-actionButton')}>
{isCollapsed ? stringFormatter.format('showAllButtonLabel', {tagCount: state.collection.size}) : stringFormatter.format('hideButtonLabel')}
</ActionButton>
}
Comment thread
reidbarber marked this conversation as resolved.
</div>
</FocusScope>
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/tag/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@

export {TagGroup} from './TagGroup';
export {Item} from '@react-stately/collections';
export type {SpectrumTagGroupProps} from '@react-types/tag';
export type {SpectrumTagGroupProps} from './TagGroup';
Loading