diff --git a/.eslintignore b/.eslintignore
index a9bfa7b1ede..ee0245f39b0 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -7,4 +7,4 @@ node_modules
packages/*/*/dist
packages/react-aria/dist
packages/react-stately/dist
-packages/dev/storybook-builder-preview/preview.js
+packages/dev/storybook-builder-parcel/preview.js
diff --git a/.storybook/custom-addons/strictmode/index.js b/.storybook/custom-addons/strictmode/index.js
new file mode 100644
index 00000000000..49415f62acf
--- /dev/null
+++ b/.storybook/custom-addons/strictmode/index.js
@@ -0,0 +1,37 @@
+import {addons, makeDecorator} from '@storybook/addons';
+import {getQueryParams} from '@storybook/client-api';
+import React, {StrictMode, useEffect, useState} from 'react';
+
+function StrictModeDecorator(props) {
+ let {children} = props;
+ let [isStrict, setStrict] = useState(getQueryParams()?.strict === 'true' || false);
+
+ useEffect(() => {
+ let channel = addons.getChannel();
+ let updateStrict = (val) => {
+ setStrict(val);
+ };
+ channel.on('strict/updated', updateStrict);
+ return () => {
+ channel.removeListener('strict/updated', updateStrict);
+ };
+ }, []);
+
+ return isStrict ? (
+
+ {children}
+
+ ) : children;
+}
+
+export const withStrictModeSwitcher = makeDecorator({
+ name: 'withStrictModeSwitcher',
+ parameterName: 'strictModeSwitcher',
+ wrapper: (getStory, context) => {
+ return (
+
+ {getStory(context)}
+
+ );
+ }
+});
diff --git a/.storybook/custom-addons/strictmode/register.js b/.storybook/custom-addons/strictmode/register.js
new file mode 100644
index 00000000000..b226fe1e0b3
--- /dev/null
+++ b/.storybook/custom-addons/strictmode/register.js
@@ -0,0 +1,40 @@
+import {addons, types} from '@storybook/addons';
+import {getQueryParams} from '@storybook/client-api';
+import React, {useEffect, useState} from 'react';
+
+const StrictModeToolBar = ({api}) => {
+ let channel = addons.getChannel();
+ let [isStrict, setStrict] = useState(getQueryParams()?.strict === 'true' || false);
+ let onChange = () => {
+ setStrict((old) => {
+ channel.emit('strict/updated', !old);
+ return !old;
+ })
+ };
+
+ useEffect(() => {
+ api.setQueryParams({
+ 'strict': isStrict
+ });
+ });
+
+ return (
+
+ );
+};
+
+addons.register('StrictModeSwitcher', (api) => {
+ addons.add('StrictModeSwitcher', {
+ title: 'Strict mode switcher',
+ type: types.TOOL,
+ //👇 Shows the Toolbar UI element if either the Canvas or Docs tab is active
+ match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)),
+ render: () =>
+ });
+});
diff --git a/.storybook/main.js b/.storybook/main.js
index 24a0f04b09d..5d73778c058 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -11,13 +11,11 @@ module.exports = {
'storybook-dark-mode',
'./custom-addons/provider/register',
'./custom-addons/descriptions/register',
- './custom-addons/theme/register'
+ './custom-addons/theme/register',
+ './custom-addons/strictmode/register'
],
typescript: {
check: false,
reactDocgen: false
- },
- reactOptions: {
- strictMode: process.env.STRICT_MODE
- },
+ }
};
diff --git a/.storybook/preview.js b/.storybook/preview.js
index 0b268d77a1b..bc696a5a6b5 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -2,6 +2,7 @@ import {configureActions} from '@storybook/addon-actions';
import React from 'react';
import {VerticalCenter} from './layout';
import {withProviderSwitcher} from './custom-addons/provider';
+import {withStrictModeSwitcher} from './custom-addons/strictmode';
// decorator order matters, the last one will be the outer most
@@ -29,5 +30,6 @@ export const decorators = [
),
+ withStrictModeSwitcher,
withProviderSwitcher
];
diff --git a/package.json b/package.json
index 049bc351fb4..5679a401113 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,6 @@
"install-16": "yarn add -W react@^16.8.0 react-dom@^16.8.0 @testing-library/react@^12 @testing-library/react-hooks@^8",
"install-17": "yarn add -W react@^17 react-dom@^17 @testing-library/react@^12 @testing-library/react-hooks@^8",
"start": "cross-env NODE_ENV=storybook start-storybook -p 9003 --ci -c '.storybook'",
- "start-strict": "cross-env NODE_ENV=storybook STRICT_MODE=1 start-storybook -p 9003 --ci -c '.storybook'",
"build:storybook": "build-storybook -c .storybook -o dist/$(git rev-parse HEAD)/storybook",
"build:storybook-16": "build-storybook -c .storybook -o dist/$(git rev-parse HEAD)/storybook-16",
"build:storybook-17": "build-storybook -c .storybook -o dist/$(git rev-parse HEAD)/storybook-17",
diff --git a/packages/@adobe/spectrum-css-temp/components/tags/index.css b/packages/@adobe/spectrum-css-temp/components/tags/index.css
index 82cffe91b12..28d36d50e42 100644
--- a/packages/@adobe/spectrum-css-temp/components/tags/index.css
+++ b/packages/@adobe/spectrum-css-temp/components/tags/index.css
@@ -13,7 +13,7 @@ governing permissions and limitations under the License.
@import '../commons/index.css';
.spectrum-Tags {
- display: inline-flex;
+ display: flex;
flex-wrap: wrap;
margin: 0;
@@ -27,7 +27,7 @@ governing permissions and limitations under the License.
--spectrum-focus-ring-border-size: var(--spectrum-tag-border-size);
display: grid;
- grid-template-columns: 1fr auto;
+ grid-template-columns: auto 1fr auto;
grid-template-areas: "icon content action";
align-items: center;
box-sizing: border-box;
@@ -58,28 +58,36 @@ governing permissions and limitations under the License.
height: calc(var(--spectrum-tag-height) - (2 * var(--spectrum-tag-border-size)));
width: var(--spectrum-global-dimension-size-300);
}
-}
-.spectrum-Tag-icon {
- grid-area: icon;
- margin-inline-end: var(--spectrum-global-dimension-size-100);
-}
+ .spectrum-Tag-cell {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: flex;
+ align-items: center;
+ }
-.spectrum-Tag-content {
- grid-area: content;
- block-size: 100%;
- line-height: calc(var(--spectrum-tag-height) - calc(var(--spectrum-tag-border-size) * 2));
- margin-inline-end: var(--spectrum-tag-padding-x);
- flex: 1 1 auto;
- font-size: var(--spectrum-tag-text-size);
- cursor: default;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- outline: none;
-}
+ .spectrum-Tag-icon {
+ grid-area: icon;
+ margin-inline-end: var(--spectrum-global-dimension-size-100);
+ }
+
+ .spectrum-Tag-content {
+ grid-area: content;
+ line-height: calc(var(--spectrum-tag-height) - calc(var(--spectrum-tag-border-size) * 2));
+ margin-inline-end: var(--spectrum-tag-padding-x);
+ flex: 1 1 auto;
+ font-size: var(--spectrum-tag-text-size);
+ cursor: default;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ outline: none;
+ }
-.tags-removable {
- margin-inline-end: 0;
+ &.is-removable {
+ .spectrum-Tag-content {
+ margin-inline-end: 0;
+ }
+ }
}
diff --git a/packages/@react-aria/table/src/useTableColumnResize.ts b/packages/@react-aria/table/src/useTableColumnResize.ts
index 5cc67a338b1..8f2c3b65850 100644
--- a/packages/@react-aria/table/src/useTableColumnResize.ts
+++ b/packages/@react-aria/table/src/useTableColumnResize.ts
@@ -12,7 +12,7 @@
import {ChangeEvent, Key, RefObject, useCallback, useRef} from 'react';
import {ColumnSize} from '@react-types/table';
-import {DOMAttributes, MoveEndEvent, MoveMoveEvent} from '@react-types/shared';
+import {DOMAttributes} from '@react-types/shared';
import {focusSafely} from '@react-aria/focus';
import {focusWithoutScrolling, mergeProps, useId} from '@react-aria/utils';
import {getColumnHeaderId} from './utils';
@@ -40,13 +40,6 @@ export interface AriaTableColumnResizeProps {
triggerRef?: RefObject,
/** If resizing is disabled. */
isDisabled?: boolean,
- /** If the resizer was moved. Different from onResize because it is always called. */
- onMove?: (e: MoveMoveEvent) => void,
- /**
- * If the resizer was moved. Different from onResizeEnd because it is always called.
- * It also carries the interaction details in the object.
- * */
- onMoveEnd?: (e: MoveEndEvent) => void,
/** Called when resizing starts. */
onResizeStart?: (widths: Map) => void,
/** Called for every resize event that results in new column sizes. */
@@ -138,7 +131,6 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st
}
deltaX *= 10;
}
- props.onMove?.(e);
// if moving up/down only, no need to resize
if (deltaX !== 0) {
columnResizeWidthRef.current += deltaX;
@@ -148,7 +140,6 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st
onMoveEnd(e) {
let {pointerType} = e;
columnResizeWidthRef.current = 0;
- props.onMoveEnd?.(e);
if (pointerType === 'mouse') {
endResize(item);
}
@@ -186,8 +177,6 @@ export function useTableColumnResize(props: AriaTableColumnResizeProps, st
} else {
nextValue = currentWidth - 10;
}
- props.onMove({pointerType: 'virtual'} as MoveMoveEvent);
- props.onMoveEnd({pointerType: 'virtual'} as MoveEndEvent);
resize(item, nextValue);
};
diff --git a/packages/@react-aria/tag/intl/en-US.json b/packages/@react-aria/tag/intl/en-US.json
index 67ff0e1312a..f7a6cb22c7c 100644
--- a/packages/@react-aria/tag/intl/en-US.json
+++ b/packages/@react-aria/tag/intl/en-US.json
@@ -1,3 +1,3 @@
{
- "remove": "Remove"
+ "remove": "Press Space or Delete to remove tag."
}
diff --git a/packages/@react-aria/tag/package.json b/packages/@react-aria/tag/package.json
index 7b7f54219c7..f8a69edc127 100644
--- a/packages/@react-aria/tag/package.json
+++ b/packages/@react-aria/tag/package.json
@@ -17,12 +17,12 @@
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
- "@react-aria/grid": "^3.5.2",
+ "@react-aria/gridlist": "^3.1.1",
"@react-aria/i18n": "^3.6.3",
"@react-aria/interactions": "^3.13.1",
"@react-aria/utils": "^3.14.2",
- "@react-stately/grid": "^3.4.2",
- "@react-types/grid": "^3.1.5",
+ "@react-stately/tag": "3.0.0-alpha.1",
+ "@react-types/button": "^3.7.0",
"@react-types/shared": "^3.16.0",
"@react-types/tag": "3.0.0-beta.1",
"@swc/helpers": "^0.4.14"
diff --git a/packages/@react-aria/tag/src/TagKeyboardDelegate.ts b/packages/@react-aria/tag/src/TagKeyboardDelegate.ts
index 3a3ed95f5b8..3f78d483f10 100644
--- a/packages/@react-aria/tag/src/TagKeyboardDelegate.ts
+++ b/packages/@react-aria/tag/src/TagKeyboardDelegate.ts
@@ -10,25 +10,26 @@
* governing permissions and limitations under the License.
*/
-import {GridCollection} from '@react-types/grid';
-import {GridKeyboardDelegate} from '@react-aria/grid';
+import {Collection, Direction, KeyboardDelegate} from '@react-types/shared';
import {Key} from 'react';
-export class TagKeyboardDelegate extends GridKeyboardDelegate> {
- getFirstKey() {
- let key = this.collection.getFirstKey();
- let item = this.collection.getItem(key);
+export class TagKeyboardDelegate implements KeyboardDelegate {
+ private collection: Collection;
+ private direction: Direction;
- return [...item.childNodes][0].key;
+ constructor(collection: Collection, direction: Direction) {
+ this.collection = collection;
+ this.direction = direction;
}
- getLastKey() {
- let key = this.collection.getLastKey();
- let item = this.collection.getItem(key);
-
- return [...item.childNodes][0].key;
+ getFirstKey() {
+ return this.collection.getFirstKey();
}
+ getLastKey() {
+ return this.collection.getLastKey();
+ }
+
getKeyRightOf(key: Key) {
return this.direction === 'rtl' ? this.getKeyAbove(key) : this.getKeyBelow(key);
}
@@ -43,27 +44,12 @@ export class TagKeyboardDelegate extends GridKeyboardDelegate extends GridKeyboardDelegate
+ clearButtonProps: AriaButtonProps
}
-export function useTag(props: TagProps, state: GridState): TagAria {
- let {isFocused} = props;
- const {
+/**
+ * Provides the behavior and accessibility implementation for a tag component.
+ * @param props - Props to be applied to the tag.
+ * @param state - State for the tag group, as returned by `useTagGroupState`.
+ */
+export function useTag(props: TagProps, state: TagGroupState): TagAria {
+ let {
+ isFocused,
allowsRemoving,
- onRemove,
item,
- tagRef,
tagRowRef
} = props;
- const stringFormatter = useLocalizedStringFormatter(intlMessages);
- const removeString = stringFormatter.format('remove');
- const labelId = useId();
- const buttonId = useId();
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
+ let removeString = stringFormatter.format('remove');
+ let labelId = useId();
+ let buttonId = useId();
- let {rowProps} = useGridRow({
+ let {rowProps, gridCellProps} = useGridListItem({
node: item
}, state, tagRowRef);
- // Don't want the row to be focusable or accessible via keyboard
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- let {tabIndex, ...otherRowProps} = rowProps;
- let {gridCellProps} = useGridCell({
- node: [...item.childNodes][0],
- focusMode: 'cell'
- }, state, tagRef);
+ // We want the group to handle keyboard navigation between tags.
+ delete rowProps.onKeyDownCapture;
+
+ let onRemove = chain(props.onRemove, state.onRemove);
- function onKeyDown(e: KeyboardEvent) {
+ let onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Delete' || e.key === 'Backspace' || e.key === ' ') {
- onRemove(item.childNodes[0].key);
+ onRemove(item.key);
e.preventDefault();
}
- }
- const pressProps = {
- onPress: () => onRemove?.(item.childNodes[0].key)
};
- isFocused = isFocused || state.selectionManager.focusedKey === item.childNodes[0].key;
+ isFocused = isFocused || state.selectionManager.focusedKey === item.key;
let domProps = filterDOMProps(props);
return {
- clearButtonProps: mergeProps(pressProps, {
+ clearButtonProps: {
'aria-label': removeString,
'aria-labelledby': `${buttonId} ${labelId}`,
- id: buttonId
- }),
+ id: buttonId,
+ onPress: () => allowsRemoving && onRemove ? onRemove(item.key) : null
+ },
labelProps: {
id: labelId
},
- tagRowProps: otherRowProps,
+ tagRowProps: {
+ ...rowProps,
+ tabIndex: (isFocused || state.selectionManager.focusedKey == null) ? 0 : -1,
+ onKeyDown: allowsRemoving ? onKeyDown : null
+ },
tagProps: mergeProps(domProps, gridCellProps, {
'aria-errormessage': props['aria-errormessage'],
- 'aria-label': props['aria-label'],
- onKeyDown: allowsRemoving ? onKeyDown : null,
- tabIndex: (isFocused || state.selectionManager.focusedKey == null) ? 0 : -1
+ 'aria-label': props['aria-label']
})
};
}
diff --git a/packages/@react-aria/tag/src/useTagGroup.ts b/packages/@react-aria/tag/src/useTagGroup.ts
index c6ca99cefe0..8c94126cc72 100644
--- a/packages/@react-aria/tag/src/useTagGroup.ts
+++ b/packages/@react-aria/tag/src/useTagGroup.ts
@@ -10,29 +10,43 @@
* governing permissions and limitations under the License.
*/
-import {DOMAttributes, DOMProps} from '@react-types/shared';
+import {AriaTagGroupProps} from '@react-types/tag';
+import {DOMAttributes} from '@react-types/shared';
import {filterDOMProps, mergeProps} from '@react-aria/utils';
-import {ReactNode, useState} from 'react';
+import {RefObject, useState} from 'react';
+import type {TagGroupState} from '@react-stately/tag';
+import {TagKeyboardDelegate} from './TagKeyboardDelegate';
import {useFocusWithin} from '@react-aria/interactions';
-
-export interface AriaTagGroupProps extends DOMProps {
- children: ReactNode,
- isReadOnly?: boolean, // removes close button
- validationState?: 'valid' | 'invalid'
-}
+import {useGridList} from '@react-aria/gridlist';
+import {useLocale} from '@react-aria/i18n';
export interface TagGroupAria {
tagGroupProps: DOMAttributes
}
-export function useTagGroup(props: AriaTagGroupProps): TagGroupAria {
+/**
+ * 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.
+ * @param props - Props to be applied to the tag group.
+ * @param state - State for the tag group, as returned by `useTagGroupState`.
+ * @param ref - A ref to a DOM element for the tag group.
+ */
+export function useTagGroup(props: AriaTagGroupProps, state: TagGroupState, ref: RefObject): TagGroupAria {
+ let {direction} = useLocale();
+ let 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
+ delete gridProps.role;
+ delete gridProps.tabIndex;
+
let [isFocusWithin, setFocusWithin] = useState(false);
let {focusWithinProps} = useFocusWithin({
onFocusWithinChange: setFocusWithin
});
let domProps = filterDOMProps(props);
return {
- tagGroupProps: mergeProps(domProps, {
+ tagGroupProps: mergeProps(gridProps, domProps, {
'aria-atomic': false,
'aria-relevant': 'additions',
'aria-live': isFocusWithin ? 'polite' : 'off',
diff --git a/packages/@react-spectrum/overlays/src/Modal.tsx b/packages/@react-spectrum/overlays/src/Modal.tsx
index 48f75e1cc9b..499d30bd65e 100644
--- a/packages/@react-spectrum/overlays/src/Modal.tsx
+++ b/packages/@react-spectrum/overlays/src/Modal.tsx
@@ -88,6 +88,7 @@ let ModalWrapper = forwardRef(function (props: ModalWrapperProps, ref: RefObject
'--spectrum-visual-viewport-height': viewport.height + 'px'
};
+ // Attach Transition's nodeRef to outer most wrapper for node.reflow: https://github.com/reactjs/react-transition-group/blob/c89f807067b32eea6f68fd6c622190d88ced82e2/src/Transition.js#L231
return (
diff --git a/packages/@react-spectrum/overlays/src/Tray.tsx b/packages/@react-spectrum/overlays/src/Tray.tsx
index daf8b634feb..149a9d6a648 100644
--- a/packages/@react-spectrum/overlays/src/Tray.tsx
+++ b/packages/@react-spectrum/overlays/src/Tray.tsx
@@ -94,6 +94,7 @@ let TrayWrapper = forwardRef(function (props: TrayWrapperProps, ref: RefObject
diff --git a/packages/@react-spectrum/overlays/src/Underlay.tsx b/packages/@react-spectrum/overlays/src/Underlay.tsx
index 04aed249546..d5d7f5e163c 100644
--- a/packages/@react-spectrum/overlays/src/Underlay.tsx
+++ b/packages/@react-spectrum/overlays/src/Underlay.tsx
@@ -11,7 +11,7 @@
*/
import {classNames} from '@react-spectrum/utils';
-import React, {Ref} from 'react';
+import React from 'react';
import underlayStyles from '@adobe/spectrum-css-temp/components/underlay/vars.css';
interface UnderlayProps {
@@ -19,11 +19,8 @@ interface UnderlayProps {
isTransparent?: boolean
}
-function Underlay({isOpen, isTransparent}: UnderlayProps, ref: Ref) {
+export function Underlay({isOpen, isTransparent}: UnderlayProps) {
return (
-
+
);
}
-
-const _Underlay = React.forwardRef(Underlay);
-export {_Underlay as Underlay};
diff --git a/packages/@react-spectrum/provider/src/Provider.tsx b/packages/@react-spectrum/provider/src/Provider.tsx
index ac1a29401a0..7243ae74f5c 100644
--- a/packages/@react-spectrum/provider/src/Provider.tsx
+++ b/packages/@react-spectrum/provider/src/Provider.tsx
@@ -18,6 +18,7 @@ import {
useStyleProps
} from '@react-spectrum/utils';
import clsx from 'clsx';
+import {Context} from './context';
import {DOMRef} from '@react-types/shared';
import {filterDOMProps} from '@react-aria/utils';
import {I18nProvider, useLocale} from '@react-aria/i18n';
@@ -30,9 +31,6 @@ import {useColorScheme, useScale} from './mediaQueries';
// @ts-ignore
import {version} from '../package.json';
-const Context = React.createContext(null);
-Context.displayName = 'ProviderContext';
-
const DEFAULT_BREAKPOINTS = {S: 640, M: 768, L: 1024, XL: 1280, XXL: 1536};
function Provider(props: ProviderProps, ref: DOMRef) {
diff --git a/packages/@react-spectrum/provider/src/context.ts b/packages/@react-spectrum/provider/src/context.ts
new file mode 100644
index 00000000000..2c104719b04
--- /dev/null
+++ b/packages/@react-spectrum/provider/src/context.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {ProviderContext} from '@react-types/provider';
+import React from 'react';
+
+// Context is placed in a separate file to avoid fast refresh issue where the old provider context values
+// are immediately replaced with the null default. Stopgap solution until we fix this in parcel.
+export const Context = React.createContext(null);
+Context.displayName = 'ProviderContext';
diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx
index 7a360ad618a..ffc6fdf1dfa 100644
--- a/packages/@react-spectrum/table/src/Resizer.tsx
+++ b/packages/@react-spectrum/table/src/Resizer.tsx
@@ -2,10 +2,11 @@
import {classNames} from '@react-spectrum/utils';
import {ColumnSize} from '@react-types/table';
import {FocusRing} from '@react-aria/focus';
+import {getInteractionModality} from '@react-aria/interactions';
import {GridNode} from '@react-types/grid';
// @ts-ignore
import intlMessages from '../intl/*.json';
-import {MoveMoveEvent} from '@react-types/shared';
+import {mergeProps} from '@react-aria/utils';
import React, {Key, RefObject} from 'react';
import styles from '@adobe/spectrum-css-temp/components/table/vars.css';
import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';
@@ -19,8 +20,7 @@ interface ResizerProps {
triggerRef: RefObject,
onResizeStart: (widths: Map) => void,
onResize: (widths: Map) => void,
- onResizeEnd: (widths: Map) => void,
- onMoveResizer: (e: MoveMoveEvent) => void
+ onResizeEnd: (widths: Map) => void
}
function Resizer(props: ResizerProps, ref: RefObject) {
@@ -33,29 +33,30 @@ function Resizer(props: ResizerProps, ref: RefObject) {
let stringFormatter = useLocalizedStringFormatter(intlMessages);
let {direction} = useLocale();
- let {inputProps, resizerProps} = useTableColumnResize({
- ...props,
- label: stringFormatter.format('columnResizer'),
- isDisabled: isEmpty,
- onMove: (e) => {
- document.body.classList.remove(classNames(styles, 'resize-ew'));
- document.body.classList.remove(classNames(styles, 'resize-e'));
- document.body.classList.remove(classNames(styles, 'resize-w'));
- if (layout.getColumnMinWidth(column.key) >= layout.getColumnWidth(column.key)) {
- document.body.classList.add(direction === 'rtl' ? classNames(styles, 'resize-w') : classNames(styles, 'resize-e'));
- } else if (layout.getColumnMaxWidth(column.key) <= layout.getColumnWidth(column.key)) {
- document.body.classList.add(direction === 'rtl' ? classNames(styles, 'resize-e') : classNames(styles, 'resize-w'));
- } else {
- document.body.classList.add(classNames(styles, 'resize-ew'));
+ let {inputProps, resizerProps} = useTableColumnResize(
+ mergeProps(props, {
+ label: stringFormatter.format('columnResizer'),
+ isDisabled: isEmpty,
+ onResize: () => {
+ document.body.classList.remove(classNames(styles, 'resize-ew'));
+ document.body.classList.remove(classNames(styles, 'resize-e'));
+ document.body.classList.remove(classNames(styles, 'resize-w'));
+ if (getInteractionModality() === 'pointer') {
+ if (layout.getColumnMinWidth(column.key) >= layout.getColumnWidth(column.key)) {
+ document.body.classList.add(direction === 'rtl' ? classNames(styles, 'resize-w') : classNames(styles, 'resize-e'));
+ } else if (layout.getColumnMaxWidth(column.key) <= layout.getColumnWidth(column.key)) {
+ document.body.classList.add(direction === 'rtl' ? classNames(styles, 'resize-e') : classNames(styles, 'resize-w'));
+ } else {
+ document.body.classList.add(classNames(styles, 'resize-ew'));
+ }
+ }
+ },
+ onResizeEnd: () => {
+ document.body.classList.remove(classNames(styles, 'resize-ew'));
+ document.body.classList.remove(classNames(styles, 'resize-e'));
+ document.body.classList.remove(classNames(styles, 'resize-w'));
}
- props.onMoveResizer(e);
- },
- onMoveEnd: () => {
- document.body.classList.remove(classNames(styles, 'resize-ew'));
- document.body.classList.remove(classNames(styles, 'resize-e'));
- document.body.classList.remove(classNames(styles, 'resize-w'));
- }
- }, state, layout, ref);
+ }), state, layout, ref);
let style = {
cursor: undefined,
diff --git a/packages/@react-spectrum/table/src/TableView.tsx b/packages/@react-spectrum/table/src/TableView.tsx
index 364274c3193..635b038c66c 100644
--- a/packages/@react-spectrum/table/src/TableView.tsx
+++ b/packages/@react-spectrum/table/src/TableView.tsx
@@ -23,7 +23,7 @@ import {
useUnwrapDOMRef
} from '@react-spectrum/utils';
import {ColumnSize, SpectrumColumnProps, SpectrumTableProps} from '@react-types/table';
-import {DOMRef, FocusableRef, MoveMoveEvent} from '@react-types/shared';
+import {DOMRef, FocusableRef} from '@react-types/shared';
import {FocusRing, FocusScope, useFocusRing} from '@react-aria/focus';
import {getInteractionModality, useHover, usePress} from '@react-aria/interactions';
import {GridNode} from '@react-types/grid';
@@ -97,7 +97,6 @@ interface TableContextValue {
onResizeStart: (widths: Map) => void,
onResize: (widths: Map) => void,
onResizeEnd: (widths: Map) => void,
- onMoveResizer: (e: MoveMoveEvent) => void,
headerMenuOpen: boolean,
setHeaderMenuOpen: (val: boolean) => void
}
@@ -361,14 +360,6 @@ function TableView(props: SpectrumTableProps, ref: DOMRef {
- if (e.pointerType === 'keyboard') {
- lastResizeInteractionModality.current = e.pointerType;
- } else {
- lastResizeInteractionModality.current = undefined;
- }
- };
let onResizeStart = useCallback((widths) => {
setIsResizing(true);
propsOnResizeStart?.(widths);
@@ -380,7 +371,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef
+
(props: SpectrumTableProps, ref: DOMRef
@@ -419,7 +409,7 @@ function TableView(props: SpectrumTableProps, ref: DOMRef {
- if (lastResizeInteractionModality.current === 'keyboard' && headerRef.current.contains(document.activeElement)) {
+ if (getInteractionModality() === 'keyboard' && headerRef.current.contains(document.activeElement)) {
document.activeElement?.scrollIntoView?.({block: 'nearest', inline: 'nearest'});
bodyRef.current.scrollLeft = headerRef.current.scrollLeft;
}
- }, [state.contentSize, headerRef, bodyRef, lastResizeInteractionModality]);
+ }, [state.contentSize, headerRef, bodyRef]);
let headerHeight = layout.getLayoutInfo('header')?.rect.height || 0;
let visibleRect = state.virtualizer.visibleRect;
@@ -662,7 +652,6 @@ function ResizableTableColumnHeader(props) {
setIsInResizeMode,
isEmpty,
onFocusedResizer,
- onMoveResizer,
isInResizeMode,
headerMenuOpen,
setHeaderMenuOpen
@@ -807,8 +796,7 @@ function ResizableTableColumnHeader(props) {
onResizeStart={onResizeStart}
onResize={onResize}
onResizeEnd={onResizeEnd}
- triggerRef={useUnwrapDOMRef(triggerRef)}
- onMoveResizer={onMoveResizer} />
+ triggerRef={useUnwrapDOMRef(triggerRef)} />
extends TagProps
{
+ state: TagGroupState
+}
+
export function Tag(props: SpectrumTagProps) {
const {
children,
@@ -35,7 +40,6 @@ export function Tag(props: SpectrumTagProps) {
let {styleProps} = useStyleProps(otherProps);
let {hoverProps, isHovered} = useHover({});
let {isFocused, isFocusVisible, focusProps} = useFocusRing({within: true});
- let tagRef = useRef();
let tagRowRef = useRef();
let {clearButtonProps, labelProps, tagProps, tagRowProps} = useTag({
...props,
@@ -43,35 +47,36 @@ export function Tag(props: SpectrumTagProps) {
allowsRemoving,
item,
onRemove,
- tagRef,
tagRowRef
}, state);
return (
-
+ ref={tagRowRef}>
+
-
{typeof children === 'string' ? {children} : children}
- {allowsRemoving && }
+
+ {allowsRemoving && }
+
@@ -79,14 +84,11 @@ export function Tag
(props: SpectrumTagProps) {
}
function TagRemoveButton(props) {
- props = useSlotProps(props, 'tagRemoveButton');
let {styleProps} = useStyleProps(props);
- let clearBtnRef = useRef();
return (
+ {...styleProps}>
diff --git a/packages/@react-spectrum/tag/src/TagGroup.tsx b/packages/@react-spectrum/tag/src/TagGroup.tsx
index 29b6df9ee21..d3f1fe0834b 100644
--- a/packages/@react-spectrum/tag/src/TagGroup.tsx
+++ b/packages/@react-spectrum/tag/src/TagGroup.tsx
@@ -12,18 +12,14 @@
import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils';
import {DOMRef} from '@react-types/shared';
-import {GridCollection, useGridState} from '@react-stately/grid';
import {mergeProps} from '@react-aria/utils';
-import React, {ReactElement, useMemo} from 'react';
+import React, {ReactElement} from 'react';
import {SpectrumTagGroupProps} from '@react-types/tag';
import styles from '@adobe/spectrum-css-temp/components/tags/vars.css';
import {Tag} from './Tag';
-import {TagKeyboardDelegate, useTagGroup} from '@react-aria/tag';
-import {useGrid} from '@react-aria/grid';
-import {useListState} from '@react-stately/list';
-import {useLocale} from '@react-aria/i18n';
import {useProviderProps} from '@react-spectrum/provider';
-
+import {useTagGroup} from '@react-aria/tag';
+import {useTagGroupState} from '@react-stately/tag';
function TagGroup(props: SpectrumTagGroupProps, ref: DOMRef) {
props = useProviderProps(props);
@@ -34,48 +30,11 @@ function TagGroup(props: SpectrumTagGroupProps, ref: DOMRef
} = props;
let domRef = useDOMRef(ref);
let {styleProps} = useStyleProps(otherProps);
- let {direction} = useLocale();
- let listState = useListState(props);
- let gridCollection = useMemo(() => new GridCollection({
- columnCount: 1, // unused, but required for grid collections
- items: [...listState.collection].map(item => {
- let childNodes = [{
- ...item,
- index: 0,
- type: 'cell'
- }];
-
- return {
- type: 'item',
- childNodes
- };
- })
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }), [listState.collection, allowsRemoving]);
- let state = useGridState({
- ...props,
- collection: gridCollection,
- focusMode: 'cell'
- });
- let keyboardDelegate = new TagKeyboardDelegate({
- collection: state.collection,
- disabledKeys: new Set(),
- ref: domRef,
- direction,
- focusMode: 'cell'
- });
- let {gridProps} = useGrid({
- ...props,
- keyboardDelegate
- }, state, domRef);
- const {tagGroupProps} = useTagGroup(props);
-
- // Don't want the grid to be focusable or accessible via keyboard
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- let {tabIndex, role, ...otherGridProps} = gridProps;
+ let state = useTagGroupState(props);
+ let {tagGroupProps} = useTagGroup(props, state, domRef);
return (
(props: SpectrumTagGroupProps, ref: DOMRef
}
role={state.collection.size ? 'grid' : null}
ref={domRef}>
- {[...gridCollection].map(item => (
+ {[...state.collection].map(item => (
- {item.childNodes[0].rendered}
+ {item.rendered}
- ))}
+ ))}
);
}
diff --git a/packages/@react-spectrum/tag/stories/TagGroup.stories.tsx b/packages/@react-spectrum/tag/stories/TagGroup.stories.tsx
index 6974b152867..84df182a7b7 100644
--- a/packages/@react-spectrum/tag/stories/TagGroup.stories.tsx
+++ b/packages/@react-spectrum/tag/stories/TagGroup.stories.tsx
@@ -10,26 +10,24 @@
* governing permissions and limitations under the License.
*/
-import {action} from '@storybook/addon-actions';
import Audio from '@spectrum-icons/workflow/Audio';
-import {Icon} from '@react-spectrum/icon';
import {Item, TagGroup} from '../src';
import React, {useState} from 'react';
import {storiesOf} from '@storybook/react';
import {Text} from '@react-spectrum/text';
+let items = [{key: '1', label: 'Cool Tag 1'}, {key: '2', label: 'Cool Tag 2'}];
+
storiesOf('TagGroup', module)
.add(
'default',
() => render({})
)
.add('icons', () => (
-
+
{item => (
-
-
-
-
+
{item.label}
)}
@@ -38,30 +36,29 @@ storiesOf('TagGroup', module)
.add(
'onRemove',
() => {
- const [items, setItems] = useState([
- {key: 1, label: 'Cool Tag 1'},
- {key: 2, label: 'Another cool tag'},
- {key: 3, label: 'This tag'},
- {key: 4, label: 'What tag?'},
- {key: 5, label: 'This tag is cool too'},
- {key: 6, label: 'Shy tag'}
+ let [items, setItems] = useState([
+ {id: 1, label: 'Cool Tag 1'},
+ {id: 2, label: 'Another cool tag'},
+ {id: 3, label: 'This tag'},
+ {id: 4, label: 'What tag?'},
+ {id: 5, label: 'This tag is cool too'},
+ {id: 6, label: 'Shy tag'}
]);
- const onRemove = (key) => {
- const newItems = [...items].filter((item) => key !== item.key.toString());
- setItems(newItems);
- action('onRemove')(key);
+
+ let removeItem = (key) => {
+ setItems(prevItems => prevItems.filter((item) => key !== item.id));
};
- return ( onRemove(key)}>
- {item => (
- - {item.label}
- )}
- );
+ return (
+
+ {item => - {item.label}
}
+
+ );
}
)
.add('wrapping', () => (
-
+
- Cool Tag 1
- Another cool tag
- This tag
@@ -74,7 +71,7 @@ storiesOf('TagGroup', module)
)
.add('label truncation', () => (
-
+
- Cool Tag 1 with a really long label
- Another long cool tag label
- This tag
@@ -83,19 +80,17 @@ storiesOf('TagGroup', module)
)
)
.add(
- 'using items prop',
+ 'dynamic items',
() => (
-
- {item =>
- - {item.label}
- }
+
+ {item => - {item.label}
}
)
);
function render(props: any = {}) {
return (
-
+
- Cool Tag 1
- Cool Tag 2
- Cool Tag 3
diff --git a/packages/@react-spectrum/tag/test/TagGroup.test.js b/packages/@react-spectrum/tag/test/TagGroup.test.js
index 37335c931ca..d5bfdf1dcca 100644
--- a/packages/@react-spectrum/tag/test/TagGroup.test.js
+++ b/packages/@react-spectrum/tag/test/TagGroup.test.js
@@ -10,8 +10,9 @@
* governing permissions and limitations under the License.
*/
-import {act, fireEvent, render} from '@react-spectrum/test-utils';
+import {act, fireEvent, render, triggerPress, within} from '@react-spectrum/test-utils';
import {Button} from '@react-spectrum/button';
+import {chain} from '@react-aria/utils';
import {Item} from '@react-stately/collections';
import {Provider} from '@react-spectrum/provider';
import React from 'react';
@@ -22,6 +23,7 @@ import userEvent from '@testing-library/user-event';
function pressKeyOnButton(key) {
return (button) => {
fireEvent.keyDown(button, {key});
+ fireEvent.keyUp(button, {key});
};
}
@@ -55,7 +57,7 @@ describe('TagGroup', function () {
});
it('provides context for Tag component', function () {
- let {container} = render(
+ let {getAllByRole} = render(
- Tag 1
- Tag 2
@@ -63,25 +65,26 @@ describe('TagGroup', function () {
);
- let tags = container.querySelectorAll('[role="gridcell"]');
+ let tags = getAllByRole('row');
expect(tags.length).toBe(3);
fireEvent.keyDown(tags[1], {key: 'Delete'});
+ fireEvent.keyUp(tags[1], {key: 'Delete'});
expect(onRemoveSpy).toHaveBeenCalledTimes(1);
});
it('has correct accessibility roles', () => {
- let tree = render(
+ let {getByRole, getAllByRole} = render(
- Tag 1
);
- let tagGroup = tree.getByRole('grid');
+ let tagGroup = getByRole('grid');
expect(tagGroup).toBeInTheDocument();
- let tags = tree.getAllByRole('row');
- let cells = tree.getAllByRole('gridcell');
+ let tags = getAllByRole('row');
+ let cells = getAllByRole('gridcell');
expect(tags).toHaveLength(cells.length);
});
@@ -93,7 +96,7 @@ describe('TagGroup', function () {
);
- let tags = getAllByRole('gridcell');
+ let tags = getAllByRole('row');
expect(tags[0]).toHaveAttribute('tabIndex', '0');
});
@@ -104,7 +107,7 @@ describe('TagGroup', function () {
${'(up/down arrows, ltr + horizontal) TagGroup'} | ${{locale: 'de-DE'}} | ${[{action: () => {userEvent.tab();}, index: 0}, {action: pressArrowDown, index: 1}, {action: pressArrowUp, index: 0}, {action: pressArrowUp, index: 2}]}
${'(up/down arrows, rtl + horizontal) TagGroup'} | ${{locale: 'ar-AE'}} | ${[{action: () => {userEvent.tab();}, index: 0}, {action: pressArrowUp, index: 2}, {action: pressArrowDown, index: 0}, {action: pressArrowDown, index: 1}]}
`('$Name shifts button focus in the correct direction on key press', function ({Name, props, orders}) {
- let tree = render(
+ let {getAllByRole} = render(
- Tag 1
@@ -114,7 +117,7 @@ describe('TagGroup', function () {
);
- let tags = tree.getAllByRole('gridcell');
+ let tags = getAllByRole('row');
orders.forEach(({action, index}, i) => {
action(document.activeElement);
expect(document.activeElement).toBe(tags[index]);
@@ -172,7 +175,7 @@ describe('TagGroup', function () {
let buttonBefore = getByLabelText('ButtonBefore');
let buttonAfter = getByLabelText('ButtonAfter');
- let tags = getAllByRole('gridcell');
+ let tags = getAllByRole('row');
act(() => {buttonBefore.focus();});
userEvent.tab();
@@ -202,7 +205,7 @@ describe('TagGroup', function () {
let buttonBefore = getByLabelText('ButtonBefore');
let buttonAfter = getByLabelText('ButtonAfter');
- let tags = getAllByRole('gridcell');
+ let tags = getAllByRole('row');
act(() => {buttonBefore.focus();});
expect(buttonBefore).toHaveFocus();
userEvent.tab();
@@ -225,7 +228,7 @@ describe('TagGroup', function () {
let buttonBefore = getByLabelText('ButtonBefore');
let buttonAfter = getByLabelText('ButtonAfter');
- let tags = getAllByRole('gridcell');
+ let tags = getAllByRole('row');
act(() => {buttonAfter.focus();});
userEvent.tab({shift: true});
expect(document.activeElement).toBe(tags[1]);
@@ -244,13 +247,12 @@ describe('TagGroup', function () {
);
let tagGroup = getByRole('grid');
- let tagRow = tagGroup.children[0];
- let tag = tagRow.children[0];
+ let tag = tagGroup.children[0];
expect(tag).not.toHaveAttribute('icon');
expect(tag).not.toHaveAttribute('unsafe_classname');
expect(tag).toHaveAttribute('class', expect.stringContaining('test-class'));
expect(tag).toHaveAttribute('class', expect.stringContaining('-item'));
- expect(tag).toHaveAttribute('role', 'gridcell');
+ expect(tag).toHaveAttribute('role', 'row');
expect(tag).toHaveAttribute('tabIndex', '0');
});
@@ -260,22 +262,49 @@ describe('TagGroup', function () {
- Tag 1
- Tag 2
+ - Tag 3
+ - Tag 4
);
- let tags = getAllByRole('gridcell');
- expect(tags.length).toBe(2);
+ let tags = getAllByRole('row');
+ expect(tags.length).toBe(4);
expect(tags[0]).toHaveAttribute('tabIndex', '0');
expect(tags[1]).toHaveAttribute('tabIndex', '0');
+ expect(tags[2]).toHaveAttribute('tabIndex', '0');
+ expect(tags[3]).toHaveAttribute('tabIndex', '0');
- act(() => tags[0].focus());
+ userEvent.tab();
expect(tags[0]).toHaveAttribute('tabIndex', '0');
expect(tags[1]).toHaveAttribute('tabIndex', '-1');
+ expect(tags[2]).toHaveAttribute('tabIndex', '-1');
+ expect(tags[3]).toHaveAttribute('tabIndex', '-1');
+ expect(document.activeElement).toBe(tags[0]);
- pressArrowRight(tags[0]);
- expect(tags[0]).toHaveAttribute('tabIndex', '-1');
- expect(tags[1]).toHaveAttribute('tabIndex', '0');
+ fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'});
+ fireEvent.keyUp(document.activeElement, {key: 'ArrowRight'});
+ expect(document.activeElement).toBe(tags[1]);
+
+ fireEvent.keyDown(document.activeElement, {key: 'ArrowLeft'});
+ fireEvent.keyUp(document.activeElement, {key: 'ArrowLeft'});
+ expect(document.activeElement).toBe(tags[0]);
+
+ fireEvent.keyDown(document.activeElement, {key: 'End'});
+ fireEvent.keyUp(document.activeElement, {key: 'End'});
+ expect(document.activeElement).toBe(tags[3]);
+
+ fireEvent.keyDown(document.activeElement, {key: 'Home'});
+ fireEvent.keyUp(document.activeElement, {key: 'Home'});
+ expect(document.activeElement).toBe(tags[0]);
+
+ fireEvent.keyDown(document.activeElement, {key: 'PageDown'});
+ fireEvent.keyUp(document.activeElement, {key: 'PageDown'});
+ expect(document.activeElement).toBe(tags[1]);
+
+ fireEvent.keyDown(document.activeElement, {key: 'PageUp'});
+ fireEvent.keyUp(document.activeElement, {key: 'PageUp'});
+ expect(document.activeElement).toBe(tags[0]);
});
it.each`
@@ -296,44 +325,76 @@ describe('TagGroup', function () {
let tag = getByText('Tag 1');
fireEvent.keyDown(tag, {key: props.keyPress});
+ fireEvent.keyUp(tag, {key: props.keyPress});
+ expect(onRemoveSpy).toHaveBeenCalledTimes(1);
+ expect(onRemoveSpy).toHaveBeenCalledWith('1');
+ });
+
+ it('should remove tag when remove button is clicked', function () {
+ let {getAllByRole} = render(
+
+
+ - Tag 1
+ - Tag 2
+ - Tag 3
+
+
+ );
+
+ let tags = getAllByRole('row');
+ triggerPress(tags[0]);
+ expect(onRemoveSpy).not.toHaveBeenCalled();
+
+ let removeButton = within(tags[0]).getByRole('button');
+ triggerPress(removeButton);
+ expect(onRemoveSpy).toHaveBeenCalledTimes(1);
expect(onRemoveSpy).toHaveBeenCalledWith('1');
});
+ it.each`
+ Name | props
+ ${'on `Delete` keypress'} | ${{keyPress: 'Delete'}}
+ ${'on `Backspace` keypress'} | ${{keyPress: 'Backspace'}}
+ ${'on `space` keypress'} | ${{keyPress: ' '}}
+ `('Can move focus after removing tag $Name', function ({Name, props}) {
+
+ function TagGroupWithDelete(props) {
+ let [items, setItems] = React.useState([
+ {id: 1, label: 'Cool Tag 1'},
+ {id: 2, label: 'Another cool tag'},
+ {id: 3, label: 'This tag'},
+ {id: 4, label: 'What tag?'},
+ {id: 5, label: 'This tag is cool too'},
+ {id: 6, label: 'Shy tag'}
+ ]);
+
+ let removeItem = (key) => {
+ setItems(prevItems => prevItems.filter((item) => key !== item.id));
+ };
+
+ return (
+
+
+ {item => - {item.label}
}
+
+
+ );
+ }
+
+ let {getAllByRole} = render(
+
+ );
- // Commented out until spectrum can provide use case for these scenarios
- // it.each`
- // Name | Component | TagComponent | props
- // ${'TagGroup'} | ${TagGroup} | ${Item} | ${{isReadOnly: true, isRemovable: true, onRemove: onRemoveSpy}}
- // `('$Name is read only', ({Component, TagComponent, props}) => {
- // let {getByText} = render(
- //
- // Tag 1
- //
- // );
- // let tag = getByText('Tag 1');
- // fireEvent.keyDown(tag, {key: 'Delete', keyCode: 46});
- // expect(onRemoveSpy).not.toHaveBeenCalledWith('Tag 1', expect.anything());
- // });
- //
- // it.each`
- // Name | Component | TagComponent | props
- // ${'Tag'} | ${TagGroup} | ${Item} | ${{validationState: 'invalid'}}
- // `('$Name can be invalid', function ({Component, TagComponent, props}) {
- // let {getByRole, debug} = render(
- //
- // Tag 1
- //
- // );
- //
- // debug();
- //
- // let tag = getByRole('row');
- // expect(tag).toHaveAttribute('aria-invalid', 'true');
- // });
+ let tags = getAllByRole('row');
+ userEvent.tab();
+ expect(document.activeElement).toBe(tags[0]);
+ fireEvent.keyDown(document.activeElement, {key: props.keyPress});
+ fireEvent.keyUp(document.activeElement, {key: props.keyPress});
+ expect(onRemoveSpy).toHaveBeenCalledTimes(1);
+ expect(onRemoveSpy).toHaveBeenCalledWith(1);
+ tags = getAllByRole('row');
+ expect(document.activeElement).toBe(tags[0]);
+ pressArrowRight(tags[0]);
+ expect(document.activeElement).toBe(tags[1]);
+ });
});
-
-// need to add test for focus after onremove
diff --git a/packages/@react-stately/tag/README.md b/packages/@react-stately/tag/README.md
new file mode 100644
index 00000000000..49c22e51ddb
--- /dev/null
+++ b/packages/@react-stately/tag/README.md
@@ -0,0 +1,3 @@
+# @react-stately/tag
+
+This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.
diff --git a/packages/@react-stately/tag/index.ts b/packages/@react-stately/tag/index.ts
new file mode 100644
index 00000000000..4e9931530d8
--- /dev/null
+++ b/packages/@react-stately/tag/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2022 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+export * from './src';
diff --git a/packages/@react-stately/tag/package.json b/packages/@react-stately/tag/package.json
new file mode 100644
index 00000000000..ce9b085151f
--- /dev/null
+++ b/packages/@react-stately/tag/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@react-stately/tag",
+ "version": "3.0.0-alpha.1",
+ "description": "Spectrum UI components in React",
+ "license": "Apache-2.0",
+ "main": "dist/main.js",
+ "module": "dist/module.js",
+ "types": "dist/types.d.ts",
+ "source": "src/index.ts",
+ "files": [
+ "dist",
+ "src"
+ ],
+ "sideEffects": false,
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/adobe/react-spectrum"
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.6.2",
+ "@react-stately/list": "^3.6.0",
+ "@react-types/tag": "3.0.0-beta.1",
+ "@swc/helpers": "^0.4.14"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/@react-stately/tag/src/index.ts b/packages/@react-stately/tag/src/index.ts
new file mode 100644
index 00000000000..a01676123b2
--- /dev/null
+++ b/packages/@react-stately/tag/src/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2022 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+export * from './useTagGroupState';
diff --git a/packages/@react-stately/tag/src/useTagGroupState.ts b/packages/@react-stately/tag/src/useTagGroupState.ts
new file mode 100644
index 00000000000..4d0663dc692
--- /dev/null
+++ b/packages/@react-stately/tag/src/useTagGroupState.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {Key} from 'react';
+import {ListState, useListState} from '@react-stately/list';
+import {TagGroupProps} from '@react-types/tag';
+
+export interface TagGroupState extends ListState{
+ onRemove?: (key: Key) => void
+}
+
+/**
+ * Provides state management for a TagGroup component.
+ */
+export function useTagGroupState(props: TagGroupProps): TagGroupState {
+ let state = useListState(props);
+
+ let onRemove = (key) => {
+ // If a tag is removed, restore focus to the tag after, or tag before if no tag after.
+ let restoreKey = state.collection.getKeyAfter(key) || state.collection.getKeyBefore(key);
+ state.selectionManager.setFocusedKey(restoreKey);
+ };
+
+ return {
+ onRemove,
+ ...state
+ };
+}
diff --git a/packages/@react-types/list/package.json b/packages/@react-types/list/package.json
index 7bec3c04578..cdc6534b883 100644
--- a/packages/@react-types/list/package.json
+++ b/packages/@react-types/list/package.json
@@ -10,7 +10,8 @@
},
"dependencies": {
"@react-aria/gridlist": "^3.1.2",
- "@react-spectrum/list": "^3.2.2"
+ "@react-spectrum/list": "^3.2.2",
+ "@react-stately/list": "^3.6.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
diff --git a/packages/@react-types/tag/package.json b/packages/@react-types/tag/package.json
index b56e4a6907c..c2ccd24b075 100644
--- a/packages/@react-types/tag/package.json
+++ b/packages/@react-types/tag/package.json
@@ -9,7 +9,6 @@
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
- "@react-stately/grid": "^3.4.2",
"@react-types/shared": "^3.16.0"
},
"peerDependencies": {
diff --git a/packages/@react-types/tag/src/index.d.ts b/packages/@react-types/tag/src/index.d.ts
index 8c6aef4ac68..abf451cf028 100644
--- a/packages/@react-types/tag/src/index.d.ts
+++ b/packages/@react-types/tag/src/index.d.ts
@@ -11,7 +11,6 @@
*/
import {AriaLabelingProps, CollectionBase, DOMProps, ItemProps, Node, StyleProps} from '@react-types/shared';
-import {GridState} from '@react-stately/grid';
import {Key, RefObject} from 'react';
export interface TagGroupProps extends Omit, 'disabledKeys'> {
@@ -21,17 +20,14 @@ export interface TagGroupProps extends Omit, 'disabledKeys'
onRemove?: (key: Key) => void
}
-export interface SpectrumTagGroupProps extends TagGroupProps, DOMProps, StyleProps, AriaLabelingProps {}
+export interface AriaTagGroupProps extends TagGroupProps, DOMProps, AriaLabelingProps {}
+
+export interface SpectrumTagGroupProps extends AriaTagGroupProps, StyleProps {}
export interface TagProps extends ItemProps {
isFocused: boolean,
allowsRemoving?: boolean,
item: Node,
onRemove?: (key: Key) => void,
- tagRef: RefObject,
tagRowRef: RefObject
}
-
-interface SpectrumTagProps extends TagProps {
- state: GridState
-}
diff --git a/yarn.lock b/yarn.lock
index 4dfcd8d0f4d..4f05e65c303 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1132,6 +1132,13 @@
dependencies:
regenerator-runtime "^0.13.11"
+"@babel/runtime@^7.6.2":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
+ integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
+ dependencies:
+ regenerator-runtime "^0.13.11"
+
"@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.12.7", "@babel/template@^7.16.7", "@babel/template@^7.3.3":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"