Skip to content

Commit cc5deac

Browse files
authored
TagGroup alpha (#1922)
TagGroup alpha
1 parent 1f2f8aa commit cc5deac

File tree

17 files changed

+775
-426
lines changed

17 files changed

+775
-426
lines changed

packages/@adobe/spectrum-css-temp/components/tags/index.css

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,22 @@ governing permissions and limitations under the License.
1313
@import '../commons/index.css';
1414

1515
.spectrum-Tags {
16-
display: inline-block;
16+
display: inherit;
1717

1818
margin: 0;
1919
padding: 0;
2020
list-style: none;
2121
}
2222

2323
.spectrum-Tags-item {
24-
display: inline-flex;
24+
display: grid;
25+
grid-template-columns: 1fr auto;
26+
grid-template-areas: "icon content action";
2527
align-items: center;
2628
box-sizing: border-box;
2729

2830
margin: calc(var(--spectrum-taggroup-tag-gap-y) / 2) calc(var(--spectrum-taggroup-tag-gap-x) / 2);
29-
padding: 0 calc(var(--spectrum-tag-padding-x) - var(--spectrum-tag-border-size));
31+
padding-inline-start:calc(var(--spectrum-tag-padding-x) - var(--spectrum-tag-border-size));
3032
block-size: var(--spectrum-tag-height);
3133
max-inline-size: 100%;
3234

@@ -44,29 +46,38 @@ governing permissions and limitations under the License.
4446
&.is-disabled {
4547
pointer-events: none;
4648
}
49+
}
4750

48-
> .spectrum-Tags-itemIcon {
49-
margin-inline-end: var(--spectrum-tag-icon-padding-x);
50-
margin-inline-start: calc(var(--spectrum-tag-deletable-border-size-key-focus) * -1);
51-
}
52-
53-
.spectrum-Tags-itemClearButton {
54-
display: flex;
55-
margin-inline-end: calc(-0.5 * var(--spectrum-tag-padding-x));
56-
margin-inline-start: var(--spectrum-tag-deletable-border-size-key-focus);
57-
block-size: var(--spectrum-alias-font-size-default);
58-
inline-size: var(--spectrum-alias-font-size-default);
59-
border-radius: var(--spectrum-alias-border-radius-small);
60-
}
51+
.spectrum-Tag-icon {
52+
grid-area: icon;
53+
margin-inline-end: var(--spectrum-global-dimension-size-100);
6154
}
6255

63-
.spectrum-Tags-itemLabel {
56+
.spectrum-Tag-content {
57+
grid-area: content;
6458
block-size: 100%;
6559
line-height: calc(var(--spectrum-tag-height) - calc(var(--spectrum-tag-border-size) * 2));
60+
margin-inline-end: var(--spectrum-tag-padding-x);
6661
flex: 1 1 auto;
6762
font-size: var(--spectrum-tag-text-size);
6863
cursor: default;
6964
overflow: hidden;
7065
white-space: nowrap;
7166
text-overflow: ellipsis;
67+
outline: none;
68+
}
69+
70+
.tags-removable {
71+
margin-inline-end: 0;
72+
}
73+
74+
.spectrum-Tag-label {
75+
grid-area: label;
7276
}
77+
78+
.spectrum-Tag-action {
79+
grid-area: action;
80+
height: calc(var(--spectrum-tag-height) - (2 * var(--spectrum-tag-border-size)));
81+
width: var(--spectrum-global-dimension-size-300);
82+
}
83+

packages/@react-aria/tag/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919
},
2020
"dependencies": {
2121
"@babel/runtime": "^7.6.2",
22+
"@react-aria/grid": "^3.0.0",
2223
"@react-aria/i18n": "^3.1.0",
2324
"@react-aria/interactions": "^3.1.0",
2425
"@react-aria/utils": "^3.1.0",
26+
"@react-stately/grid": "^3.0.0",
27+
"@react-types/grid": "^3.0.0",
2528
"@react-types/shared": "^3.1.0",
2629
"@react-types/tag": "3.0.0-alpha.1"
2730
},
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2020 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {GridCollection} from '@react-types/grid';
14+
import {GridKeyboardDelegate} from '@react-aria/grid';
15+
import {Key} from 'react';
16+
17+
export class TagKeyboardDelegate<T> extends GridKeyboardDelegate<T, GridCollection<T>> {
18+
getKeyRightOf(key: Key) {
19+
return this.direction === 'rtl' ? this.getKeyAbove(key) : this.getKeyBelow(key);
20+
}
21+
22+
getKeyLeftOf(key: Key) {
23+
return this.direction === 'rtl' ? this.getKeyBelow(key) : this.getKeyAbove(key);
24+
}
25+
26+
getKeyBelow(key) {
27+
let startItem = this.collection.getItem(key);
28+
if (!startItem) {
29+
return;
30+
}
31+
32+
// If focus was on a cell, start searching from the parent row
33+
if (this.isCell(startItem)) {
34+
key = startItem.parentKey;
35+
}
36+
37+
// Find the next item
38+
key = this.findNextKey(key);
39+
if (key != null) {
40+
// If focus was on a cell, focus the cell with the same index in the next row.
41+
if (this.isCell(startItem)) {
42+
let item = this.collection.getItem(key);
43+
let newKey = [...item.childNodes][startItem.index].key;
44+
45+
// Ignore disabled tags
46+
if (this.disabledKeys.has(newKey)) {
47+
return this.getKeyBelow(newKey);
48+
}
49+
return newKey;
50+
}
51+
52+
// Otherwise, focus the next row
53+
if (this.focusMode === 'row') {
54+
return key;
55+
}
56+
}
57+
}
58+
59+
getKeyAbove(key) {
60+
let startItem = this.collection.getItem(key);
61+
if (!startItem) {
62+
return;
63+
}
64+
65+
// If focus is on a cell, start searching from the parent row
66+
if (this.isCell(startItem)) {
67+
key = startItem.parentKey;
68+
}
69+
70+
// Find the previous item
71+
key = this.findPreviousKey(key);
72+
if (key != null) {
73+
// If focus was on a cell, focus the cell with the same index in the previous row.
74+
if (this.isCell(startItem)) {
75+
let item = this.collection.getItem(key);
76+
let newKey = [...item.childNodes][startItem.index].key;
77+
78+
// ignore disabled tags
79+
if (this.disabledKeys.has(newKey)) {
80+
return this.getKeyAbove(newKey);
81+
}
82+
return newKey;
83+
}
84+
85+
// Otherwise, focus the previous row
86+
if (this.focusMode === 'row') {
87+
return key;
88+
}
89+
}
90+
}
91+
}

packages/@react-aria/tag/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
export * from './TagKeyboardDelegate';
1314
export * from './useTag';
1415
export * from './useTagGroup';

packages/@react-aria/tag/src/useTag.ts

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,74 +10,83 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {ButtonHTMLAttributes, HTMLAttributes, KeyboardEvent, ReactNode} from 'react';
14-
import {DOMProps, Removable} from '@react-types/shared';
13+
import {ButtonHTMLAttributes, HTMLAttributes, KeyboardEvent} from 'react';
1514
import {filterDOMProps, mergeProps, useId} from '@react-aria/utils';
15+
import {GridState} from '@react-stately/grid';
1616
// @ts-ignore
1717
import intlMessages from '../intl/*.json';
18+
import {TagProps} from '@react-types/tag';
19+
import {useGridCell, useGridRow} from '@react-aria/grid';
1820
import {useMessageFormatter} from '@react-aria/i18n';
1921

2022

21-
export interface AriaTagProps extends Removable<ReactNode, void>, DOMProps {
22-
children?: ReactNode,
23-
isDisabled?: boolean,
24-
validationState?: 'invalid' | 'valid',
25-
isSelected?: boolean,
26-
role?: string
27-
}
28-
2923
export interface TagAria {
30-
tagProps: HTMLAttributes<HTMLElement>,
3124
labelProps: HTMLAttributes<HTMLElement>,
25+
tagProps: HTMLAttributes<HTMLElement>,
26+
tagRowProps: HTMLAttributes<HTMLElement>,
3227
clearButtonProps: ButtonHTMLAttributes<HTMLButtonElement>
3328
}
3429

35-
export function useTag(props: AriaTagProps): TagAria {
30+
export function useTag(props: TagProps<any>, state: GridState<any, any>): TagAria {
31+
let {isFocused} = props;
3632
const {
3733
isDisabled,
38-
validationState,
3934
isRemovable,
40-
isSelected,
4135
onRemove,
4236
children,
43-
role
37+
item,
38+
tagRef,
39+
tagRowRef
4440
} = props;
4541
const formatMessage = useMessageFormatter(intlMessages);
4642
const removeString = formatMessage('remove');
47-
const tagId = useId();
43+
const labelId = useId();
4844
const buttonId = useId();
4945

46+
let {rowProps} = useGridRow({
47+
node: item
48+
}, state, tagRowRef);
49+
// Don't want the row to be focusable or accessible via keyboard
50+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
51+
let {tabIndex, ...otherRowProps} = rowProps;
52+
53+
let {gridCellProps} = useGridCell({
54+
node: [...item.childNodes][0],
55+
focusMode: 'cell'
56+
}, state, tagRef);
57+
5058
function onKeyDown(e: KeyboardEvent<HTMLElement>) {
5159
if (e.key === 'Delete' || e.key === 'Backspace') {
5260
onRemove(children, e);
5361
e.preventDefault();
5462
}
5563
}
5664
const pressProps = {
57-
onPress: e => onRemove(children, e)
65+
onPress: e => onRemove?.(children, e)
5866
};
5967

68+
isFocused = isFocused || state.selectionManager.focusedKey === item.childNodes[0].key;
6069
let domProps = filterDOMProps(props);
6170
return {
62-
tagProps: mergeProps(domProps, {
63-
'aria-selected': !isDisabled && isSelected,
64-
'aria-invalid': validationState === 'invalid' || undefined,
65-
'aria-errormessage': props['aria-errormessage'],
66-
onKeyDown: !isDisabled && isRemovable ? onKeyDown : null,
67-
role: role === 'gridcell' ? 'row' : null,
68-
tabIndex: isDisabled ? -1 : 0
69-
}),
70-
labelProps: {
71-
id: tagId,
72-
role
73-
},
7471
clearButtonProps: mergeProps(pressProps, {
7572
'aria-label': removeString,
76-
'aria-labelledby': `${buttonId} ${tagId}`,
73+
'aria-labelledby': `${buttonId} ${labelId}`,
7774
id: buttonId,
78-
title: removeString,
79-
isDisabled,
80-
role
75+
isDisabled
76+
}),
77+
labelProps: {
78+
id: labelId
79+
},
80+
tagRowProps: otherRowProps,
81+
tagProps: mergeProps(domProps, gridCellProps, {
82+
'aria-errormessage': props['aria-errormessage'],
83+
'aria-label': props['aria-label'],
84+
onKeyDown: !isDisabled && isRemovable ? onKeyDown : null,
85+
tabIndex: (isFocused || state.selectionManager.focusedKey == null) && !isDisabled ? 0 : -1,
86+
onFocus() {
87+
state.selectionManager.setFocusedKey(item.childNodes[0].key);
88+
},
89+
ref: tagRef
8190
})
8291
};
8392
}

packages/@react-aria/tag/src/useTagGroup.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212

1313
import {DOMProps} from '@react-types/shared';
1414
import {filterDOMProps, mergeProps} from '@react-aria/utils';
15-
import {HTMLAttributes, useState} from 'react';
15+
import {HTMLAttributes, Key, ReactNode, useState} from 'react';
1616
import {useFocusWithin} from '@react-aria/interactions';
1717

1818
interface AriaTagGroupProps extends DOMProps {
19+
children: ReactNode,
20+
disabledKeys?: Iterable<Key>,
1921
isDisabled?: boolean,
2022
isReadOnly?: boolean, // removes close button
2123
validationState?: 'valid' | 'invalid'
@@ -25,21 +27,23 @@ interface TagGroupAria {
2527
tagGroupProps: HTMLAttributes<HTMLElement>
2628
}
2729

28-
export function useTagGroup(props: AriaTagGroupProps): TagGroupAria {
29-
const {isDisabled} = props;
30+
export function useTagGroup(props: AriaTagGroupProps, listState): TagGroupAria {
31+
let {isDisabled} = props;
3032
let [isFocusWithin, setFocusWithin] = useState(false);
3133
let {focusWithinProps} = useFocusWithin({
3234
onFocusWithinChange: setFocusWithin
3335
});
34-
36+
let allKeys = [...listState.collection.getKeys()];
37+
if (!allKeys.some(key => !listState.disabledKeys.has(key))) {
38+
isDisabled = true;
39+
}
3540
let domProps = filterDOMProps(props);
3641
return {
3742
tagGroupProps: mergeProps(domProps, {
38-
role: 'grid',
3943
'aria-atomic': false,
4044
'aria-relevant': 'additions',
4145
'aria-live': isFocusWithin ? 'polite' : 'off',
42-
'aria-disabled': isDisabled,
46+
'aria-disabled': isDisabled === true,
4347
...focusWithinProps
4448
} as HTMLAttributes<HTMLElement>)
4549
};

0 commit comments

Comments
 (0)