diff --git a/packages/dropdowns.next/.size-snapshot.json b/packages/dropdowns.next/.size-snapshot.json
index 59c5e80d9e7..b71158ed689 100644
--- a/packages/dropdowns.next/.size-snapshot.json
+++ b/packages/dropdowns.next/.size-snapshot.json
@@ -1,20 +1,20 @@
{
"index.cjs.js": {
- "bundled": 55344,
- "minified": 40274,
- "gzipped": 9098
+ "bundled": 55890,
+ "minified": 40577,
+ "gzipped": 9273
},
"index.esm.js": {
- "bundled": 50695,
- "minified": 35869,
- "gzipped": 8627,
+ "bundled": 51241,
+ "minified": 36172,
+ "gzipped": 8697,
"treeshaked": {
"rollup": {
- "code": 28209,
+ "code": 28490,
"import_statements": 1064
},
"webpack": {
- "code": 31080
+ "code": 31363
}
}
}
diff --git a/packages/dropdowns.next/src/elements/combobox/Combobox.spec.tsx b/packages/dropdowns.next/src/elements/combobox/Combobox.spec.tsx
index 843ee001541..85049f567f2 100644
--- a/packages/dropdowns.next/src/elements/combobox/Combobox.spec.tsx
+++ b/packages/dropdowns.next/src/elements/combobox/Combobox.spec.tsx
@@ -220,10 +220,14 @@ describe('Combobox', () => {
it('renders non-editable as expected', () => {
const { getByTestId } = render();
+ const combobox = getByTestId('combobox');
const input = getByTestId('input');
+ expect(combobox).toHaveAttribute('tabIndex', '-1');
expect(input).toHaveAttribute('readonly');
expect(input).toHaveAttribute('hidden');
+ expect(input).toHaveAttribute('aria-hidden', 'true');
+ expect(input).toHaveStyleRule('display', 'none', { modifier: '&[aria-hidden="true"]' });
});
it('renders `isMultiselectable` as expected', () => {
@@ -349,6 +353,41 @@ describe('Combobox', () => {
expect(button).toHaveTextContent('test-1');
});
+ it('handles tag group expansion as expected', async () => {
+ const tagProps = { 'data-test-id': 'tag' } as HTMLAttributes;
+ const { getByTestId } = render(
+
+
+
+
+ );
+ const combobox = getByTestId('combobox');
+ const trigger = combobox.firstChild as HTMLElement;
+ const input = getByTestId('input');
+ const tag = getByTestId('tag');
+ const button = tag.nextSibling as HTMLElement;
+
+ expect(tag).toHaveAttribute('hidden');
+ expect(button).not.toHaveAttribute('hidden');
+
+ await user.click(button);
+
+ expect(tag).not.toHaveAttribute('hidden');
+ expect(button).toHaveAttribute('hidden');
+ expect(input).toHaveFocus();
+
+ await user.keyboard('{Tab}');
+
+ expect(tag).toHaveAttribute('hidden');
+ expect(button).not.toHaveAttribute('hidden');
+
+ await user.click(trigger);
+
+ expect(tag).not.toHaveAttribute('hidden');
+ expect(button).toHaveAttribute('hidden');
+ expect(input).toHaveFocus();
+ });
+
it('handles `renderValue` as expected', () => {
const { getByTestId } = render(
`test-${(selection as ISelectedOption).value}`}>
diff --git a/packages/dropdowns.next/src/elements/combobox/Combobox.tsx b/packages/dropdowns.next/src/elements/combobox/Combobox.tsx
index 7c369a62c28..63e57a80973 100644
--- a/packages/dropdowns.next/src/elements/combobox/Combobox.tsx
+++ b/packages/dropdowns.next/src/elements/combobox/Combobox.tsx
@@ -18,7 +18,7 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import { ThemeContext } from 'styled-components';
-import { IOption, IUseComboboxReturnValue, useCombobox } from '@zendeskgarden/container-combobox';
+import { IUseComboboxReturnValue, useCombobox } from '@zendeskgarden/container-combobox';
import { DEFAULT_THEME, useText, useWindow } from '@zendeskgarden/react-theming';
import { VALIDATION } from '@zendeskgarden/react-forms';
import ChevronIcon from '@zendeskgarden/svg-icons/src/16/chevron-down-stroke.svg';
@@ -31,13 +31,13 @@ import {
StyledInputIcon,
StyledInput,
StyledInputGroup,
+ StyledTagsButton,
StyledTrigger,
StyledValue
} from '../../views';
-import { StyledTagsButton } from '../../views/combobox/StyledTagsButton';
import { Listbox } from './Listbox';
-import { Tag } from './Tag';
-import { toOptions, toString } from './utils';
+import { TagGroup } from './TagGroup';
+import { toOptions } from './utils';
const MAX_TAGS = 4;
@@ -82,6 +82,7 @@ export const Combobox = forwardRef(
) => {
const { hasHint, hasMessage, labelProps, setLabelProps } = useFieldContext();
const [isLabelHovered, setIsLabelHovered] = useState(false);
+ const [isTagGroupExpanded, setIsTagGroupExpanded] = useState(false);
const [optionTagProps, setOptionTagProps] = useState>(
{}
);
@@ -99,7 +100,6 @@ export const Combobox = forwardRef(
const triggerRef = useRef(null);
const inputRef = useRef(null);
const listboxRef = useRef(null);
- const tagsButtonRef = useRef(null);
/* istanbul ignore next */
const theme = useContext(ThemeContext) || DEFAULT_THEME;
const environment = useWindow(theme);
@@ -178,10 +178,18 @@ export const Combobox = forwardRef(
...(getTriggerProps({
onFocus: () => {
hasFocus.current = true;
+
+ if (isMultiselectable) {
+ setIsTagGroupExpanded(true);
+ }
},
onBlur: event => {
if (event.relatedTarget === null || !triggerRef.current?.contains(event.relatedTarget)) {
hasFocus.current = false;
+
+ if (isMultiselectable) {
+ setIsTagGroupExpanded(false);
+ }
}
}
}) as HTMLAttributes)
@@ -191,6 +199,7 @@ export const Combobox = forwardRef(
hidden: !(isEditable && hasFocus.current),
isBare,
isCompact,
+ isEditable,
isMultiselectable,
placeholder,
...(getInputProps({
@@ -215,47 +224,14 @@ export const Combobox = forwardRef(
return () => labelProps && setLabelProps(undefined);
}, [getLabelProps, labelProps, setLabelProps]);
- const Tags = ({ selectedOptions }: { selectedOptions: IOption[] }) => {
- const value = selectedOptions.length - maxTags;
-
- return (
- <>
- {selectedOptions.map((option, index) => {
- const key = toString(option);
- const disabled = isDisabled || option.disabled;
- const hidden = !hasFocus.current && index >= maxTags;
-
- return (
-
- );
- })}
- {!hasFocus.current && selectedOptions.length > maxTags && (
- isEditable && inputRef.current?.focus()}
- tabIndex={-1}
- type="button"
- ref={tagsButtonRef}
- >
- {renderExpandTags
- ? renderExpandTags(value)
- : expandTags?.replace('{{value}}', value.toString())}
-
- )}
- >
- );
- };
-
return (
-
+
{startIcon && (
@@ -265,7 +241,37 @@ export const Combobox = forwardRef(
)}
{isMultiselectable && Array.isArray(selection) && (
-
+
+ {selection.length > maxTags && (
+ {
+ if (isEditable) {
+ event.stopPropagation();
+ inputRef.current?.focus();
+ }
+ }}
+ tabIndex={-1}
+ type="button"
+ >
+ {(() => {
+ const value = selection.length - maxTags;
+
+ return renderExpandTags
+ ? renderExpandTags(value)
+ : expandTags?.replace('{{value}}', value.toString());
+ })()}
+
+ )}
+
)}
{!(isEditable && hasFocus.current) && (
) => (
+ <>
+ {selection.map((option, index) => {
+ const key = toString(option);
+ const disabled = isDisabled || option.disabled;
+
+ return (
+ = maxTags}
+ option={{ ...option, disabled }}
+ tooltipZIndex={listboxZIndex ? listboxZIndex + 1 : undefined}
+ {...optionTagProps[key]}
+ />
+ );
+ })}
+ {children}
+ >
+);
+
+TagGroup.displayName = 'TagGroup';
diff --git a/packages/dropdowns.next/src/types/index.ts b/packages/dropdowns.next/src/types/index.ts
index 642a14c48ac..d7612cd31ec 100644
--- a/packages/dropdowns.next/src/types/index.ts
+++ b/packages/dropdowns.next/src/types/index.ts
@@ -237,3 +237,18 @@ export interface ITagProps extends Omit {
/** @ignore Sets the `z-index` of the tooltip */
tooltipZIndex?: number;
}
+
+export interface ITagGroupProps {
+ /** Indicates that the tag group is not interactive */
+ isDisabled?: boolean;
+ /** Determines tag group expansion */
+ isExpanded: boolean;
+ /** Indicates the `z-index` of the listbox */
+ listboxZIndex?: number;
+ /** Determines the maximum number of tags displayed when the tag group is collapsed */
+ maxTags: number;
+ /** Provides tag props for the associated option */
+ optionTagProps: Record;
+ /** Provides the current selection */
+ selection: IOption[];
+}
diff --git a/packages/dropdowns.next/src/views/combobox/StyledInput.ts b/packages/dropdowns.next/src/views/combobox/StyledInput.ts
index 370e38dc6ed..1f695c8c2b2 100644
--- a/packages/dropdowns.next/src/views/combobox/StyledInput.ts
+++ b/packages/dropdowns.next/src/views/combobox/StyledInput.ts
@@ -19,6 +19,7 @@ const COMPONENT_ID = 'dropdowns.combobox.input';
interface IStyledInputProps extends ThemeProps {
isBare?: boolean;
isCompact?: boolean;
+ isEditable?: boolean;
isMultiselectable?: boolean;
}
@@ -84,7 +85,11 @@ export const StyledInput = styled.input.attrs({
&[hidden] {
display: revert;
- ${hideVisually()}
+ ${props => props.isEditable && hideVisually()}
+ }
+
+ &[aria-hidden='true'] {
+ display: none;
}
${props => retrieveComponentStyles(COMPONENT_ID, props)};
diff --git a/packages/dropdowns.next/src/views/index.ts b/packages/dropdowns.next/src/views/index.ts
index f3bfff2e2ed..5474e300998 100644
--- a/packages/dropdowns.next/src/views/index.ts
+++ b/packages/dropdowns.next/src/views/index.ts
@@ -24,6 +24,7 @@ export * from './combobox/StyledOptionIcon';
export * from './combobox/StyledOptionMeta';
export * from './combobox/StyledOptionTypeIcon';
export * from './combobox/StyledTag';
+export * from './combobox/StyledTagsButton';
export * from './combobox/StyledTrigger';
export * from './combobox/StyledValue';
export * from './menu/StyledFloatingMenu';