diff --git a/.changeset/core-4107-input-group-noninteractive-roles.md b/.changeset/core-4107-input-group-noninteractive-roles.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/core-4107-input-group-noninteractive-roles.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/packages/mosaic/src/components/input-group/input-group.test.tsx b/packages/mosaic/src/components/input-group/input-group.test.tsx
index 733ff691d92..c14da7c64fd 100644
--- a/packages/mosaic/src/components/input-group/input-group.test.tsx
+++ b/packages/mosaic/src/components/input-group/input-group.test.tsx
@@ -42,15 +42,20 @@ describe('Mosaic InputGroup', () => {
Decoration
+ 3/20
+ Optional
+ |
,
);
const input = screen.getByRole('textbox');
fireEvent.click(screen.getByTestId('group'));
expect(input).toHaveFocus();
- input.blur();
- fireEvent.click(screen.getByText('Decoration'));
- expect(input).toHaveFocus();
+ for (const text of ['Decoration', '3/20', 'Optional', '|']) {
+ input.blur();
+ fireEvent.click(screen.getByText(text));
+ expect(input).toHaveFocus();
+ }
});
it('does not focus the input when the group click is canceled or disabled', () => {
@@ -93,10 +98,16 @@ describe('Mosaic InputGroup', () => {
>
Custom control
+
+ Toggle
+
,
);
- for (const name of ['Action', 'Link', 'Custom control']) {
+ for (const name of ['Action', 'Link', 'Custom control', 'Toggle']) {
await user.click(screen.getByText(name));
expect(screen.getByRole('textbox')).not.toHaveFocus();
}
diff --git a/packages/mosaic/src/components/input-group/input-group.tsx b/packages/mosaic/src/components/input-group/input-group.tsx
index 8e33294bc6a..b70a337f5f7 100644
--- a/packages/mosaic/src/components/input-group/input-group.tsx
+++ b/packages/mosaic/src/components/input-group/input-group.tsx
@@ -21,6 +21,48 @@ export interface InputGroupRootProps extends MosaicComponentProps<'div'> {
size?: InputGroupSize;
}
+const widgetRoles = [
+ 'button',
+ 'checkbox',
+ 'combobox',
+ 'grid',
+ 'gridcell',
+ 'link',
+ 'listbox',
+ 'menu',
+ 'menubar',
+ 'menuitem',
+ 'menuitemcheckbox',
+ 'menuitemradio',
+ 'option',
+ 'radio',
+ 'radiogroup',
+ 'scrollbar',
+ 'searchbox',
+ 'slider',
+ 'spinbutton',
+ 'switch',
+ 'tab',
+ 'tablist',
+ 'textbox',
+ 'tree',
+ 'treegrid',
+ 'treeitem',
+];
+
+const interactiveSelector = [
+ 'button',
+ 'input',
+ 'select',
+ 'textarea',
+ 'a[href]',
+ 'label',
+ 'summary',
+ '[tabindex]',
+ ...widgetRoles.map(role => `[role="${role}"]`),
+ '[contenteditable]:not([contenteditable="false"])',
+].join(', ');
+
const Root = React.forwardRef(function MosaicInputGroupRoot(
{ render, xstyle, onClick, disabled: disabledProp, invalid: invalidProp, size = 'md', ...otherProps },
ref,
@@ -52,9 +94,7 @@ const Root = React.forwardRef(function Mosa
if (event.defaultPrevented || disabled || !(event.target instanceof Element)) {
return;
}
- const interactive = event.target.closest(
- 'button, input, select, textarea, a[href], label, summary, [tabindex], [role], [contenteditable]:not([contenteditable="false"])',
- );
+ const interactive = event.target.closest(interactiveSelector);
if (
!event.currentTarget.contains(event.target) ||
(interactive && interactive !== event.currentTarget && event.currentTarget.contains(interactive))
diff --git a/packages/swingset/src/stories/input-group.mdx b/packages/swingset/src/stories/input-group.mdx
index d7b89302317..e75e811a07e 100644
--- a/packages/swingset/src/stories/input-group.mdx
+++ b/packages/swingset/src/stories/input-group.mdx
@@ -81,3 +81,12 @@ function PasswordField() {
name='Invalid'
storyModule={InputGroupStories}
/>
+
+### Status text
+
+Content with a noninteractive role, such as a live character count, focuses the input when clicked, like plain text.
+
+
diff --git a/packages/swingset/src/stories/input-group.stories.tsx b/packages/swingset/src/stories/input-group.stories.tsx
index c80af7431ec..4c8ca9d6898 100644
--- a/packages/swingset/src/stories/input-group.stories.tsx
+++ b/packages/swingset/src/stories/input-group.stories.tsx
@@ -151,3 +151,28 @@ export function Invalid(props: Record) {
);
}
+
+export function Counter(props: Record) {
+ const [value, setValue] = useState('');
+
+ return (
+
+ Display name
+
+ setValue(event.target.value)}
+ />
+
+
+ {value.length}/20
+
+
+
+
+ );
+}