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 + + + + + ); +}