Skip to content

Commit 52abf0f

Browse files
committed
fix(Dialog): focus on first primary button or input with autofocus
1 parent d143076 commit 52abf0f

File tree

5 files changed

+48
-9
lines changed

5 files changed

+48
-9
lines changed

.changeset/giant-monkeys-sing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@cube-dev/ui-kit': minor
33
---
44

5-
When a dialog is opened focus on the first primary button.
5+
When a dialog is opened focus on the first input with autofocus or primary button.

src/components/fields/TextInput/TextInputBase.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ function _TextInputBase(props: CubeTextInputBaseProps, ref) {
353353
rows={multiLine ? rows : undefined}
354354
mods={modifiers}
355355
style={textSecurityStyles}
356+
data-autofocus={autoFocus ? '' : undefined}
356357
autoFocus={autoFocus}
357358
data-size={size}
358359
autocomplete={autocomplete}

src/components/overlays/Dialog/Dialog.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,11 @@ export const Dialog = forwardRef(function Dialog(
163163

164164
return (
165165
// This component is actually traps the focus inside the dialog.
166-
<FocusLock returnFocus disabled={!isEntered || context.type === 'panel'}>
167-
{/* FocusScope has a bug that prevents selection and blurring in the dialog. */}
168-
{/* But we need it to make the autofocus work. */}
166+
<FocusLock
167+
returnFocus
168+
autoFocus={false}
169+
disabled={!isEntered || context.type === 'panel'}
170+
>
169171
{content}
170172
</FocusLock>
171173
);
@@ -217,7 +219,7 @@ const DialogContent = forwardRef(function DialogContent(
217219
) {
218220
(
219221
domRef.current.querySelector(
220-
'[data-qa="ButtonGroup"] button[data-type="primary"]',
222+
'input[data-autofocus], [data-qa="ButtonGroup"] button[data-type="primary"]',
221223
) as HTMLButtonElement
222224
)?.focus();
223225
}

src/components/overlays/Dialog/DialogTrigger.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Placement,
1111
} from 'react-aria';
1212

13+
import { useIsFirstRender } from '../../../_internal/index';
1314
import { useCombinedRefs } from '../../../utils/react/index';
1415
import { Modal, Popover, Tray, WithCloseBehavior } from '../Modal';
1516
import { Styles } from '../../../tasty';
@@ -229,7 +230,7 @@ function PopoverTrigger(allProps) {
229230
...props
230231
} = allProps;
231232

232-
let triggerRef = useRef<HTMLElement>(null);
233+
let triggerRef = useRef<HTMLButtonElement>(null);
233234
let overlayRef = useRef<HTMLDivElement>(null);
234235

235236
let {
@@ -299,6 +300,7 @@ function PopoverTrigger(allProps) {
299300

300301
function DialogTriggerBase(props) {
301302
const ref = useCombinedRefs(props.ref);
303+
const isFirstRender = useIsFirstRender();
302304

303305
let {
304306
type,
@@ -309,7 +311,6 @@ function DialogTriggerBase(props) {
309311
triggerProps = {},
310312
overlay,
311313
trigger,
312-
hideOnClose,
313314
} = props;
314315

315316
let context = {
@@ -320,9 +321,9 @@ function DialogTriggerBase(props) {
320321
...dialogProps,
321322
};
322323

323-
// Restore focus manually when the dialog closes and has `hideOnClose` set to true
324+
// Restore focus manually when the dialog closes
324325
useEffect(() => {
325-
if (!state.isOpen && hideOnClose) {
326+
if (!state.isOpen && !isFirstRender) {
326327
setTimeout(() => {
327328
ref.current?.focus();
328329
});

src/components/overlays/Dialog/stories/Dialog.stories.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
Title,
1919
Space,
2020
DirectionIcon,
21+
TextInput,
2122
} from '../../../../index';
2223
import { timeout } from '../../../../utils/promise';
2324
import { baseProps } from '../../../../stories/lists/baseProps';
@@ -65,6 +66,37 @@ const Template: StoryFn<
6566
);
6667
};
6768

69+
const TemplateWithInput: StoryFn<
70+
CubeDialogTriggerProps & { size: CubeDialogProps['size'] }
71+
> = ({ size, styles, ...props }) => {
72+
return (
73+
<DialogTrigger {...props}>
74+
<Button>Click me!</Button>
75+
{(close) => (
76+
<Dialog size={size} styles={styles}>
77+
<Header>
78+
<Title>Modal title</Title>
79+
<Text>Header</Text>
80+
</Header>
81+
<Content>
82+
<TextInput autoFocus label="Text input" />
83+
</Content>
84+
<Footer>
85+
<Button.Group>
86+
<Button type="primary" onPress={close}>
87+
Action
88+
</Button>
89+
<Button onPress={close}>Sec</Button>
90+
<Button onPress={close}>Cancel</Button>
91+
</Button.Group>
92+
<Text>Footer</Text>
93+
</Footer>
94+
</Dialog>
95+
)}
96+
</DialogTrigger>
97+
);
98+
};
99+
68100
const WithTriggerStateTemplate: StoryFn<
69101
CubeDialogTriggerProps & { size: CubeDialogProps['size'] }
70102
> = ({ size, styles, ...props }) => {
@@ -95,6 +127,9 @@ Default.play = async ({ canvasElement, viewMode }) => {
95127
await expect(await findByRole('dialog')).toBeInTheDocument();
96128
};
97129

130+
export const WithInput = TemplateWithInput.bind({});
131+
WithInput.play = Default.play;
132+
98133
export const Modal: typeof Template = Template.bind({});
99134
Modal.args = {
100135
type: 'modal',

0 commit comments

Comments
 (0)