Skip to content

Commit c9f76f5

Browse files
authored
fix(Dialog): remove underlay from type panel dialogs (#567)
1 parent cd06c0d commit c9f76f5

File tree

12 files changed

+121
-32
lines changed

12 files changed

+121
-32
lines changed

.changeset/eleven-foxes-shake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cube-dev/ui-kit': minor
3+
---
4+
5+
Remove underlay from dialogs with type `panel`.

.changeset/metal-worms-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cube-dev/ui-kit': minor
3+
---
4+
5+
Add ReturnIcon component.

src/components/fields/TextArea/TextArea.stories.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ WithIcon.args = { icon: true };
3838

3939
export const Password = Template.bind({});
4040
Password.args = { icon: true, type: 'password', defaultValue: 'hidden value' };
41+
42+
export const AutoSize = Template.bind({});
43+
AutoSize.args = { autoSize: true, defaultValue: '1\n2\n3\n4', rows: 1 };

src/components/fields/TextArea/TextArea.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function TextArea(props: WithNullableValue<CubeTextAreaProps>, ref) {
7070
if (inputRef.current) {
7171
onHeightChange();
7272
}
73-
}, [onHeightChange, inputValue, inputRef]);
73+
}, [inputValue, inputRef.current]);
7474

7575
let { labelProps, inputProps } = useTextField(
7676
{

src/components/overlays/Dialog/Dialog.tsx

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import styled from 'styled-components';
21
import { useDOMRef } from '@react-spectrum/utils';
3-
import { DismissButton } from 'react-aria';
4-
import { forwardRef, ReactElement } from 'react';
2+
import { DismissButton, FocusScope, useFocusManager } from 'react-aria';
3+
import { forwardRef, ReactElement, useEffect, useMemo, useState } from 'react';
54
import { useDialog, useMessageFormatter, AriaDialogProps } from 'react-aria';
65
import { DOMRef } from '@react-types/shared';
7-
import FocusLock from 'react-focus-lock';
86

97
import {
108
BASE_STYLES,
@@ -31,35 +29,40 @@ const DialogElement = tasty({
3129
as: 'section',
3230
styles: {
3331
pointerEvents: 'auto',
34-
position: 'relative',
32+
position: {
33+
'': 'relative',
34+
'[data-type="panel"]': 'absolute',
35+
},
3536
display: 'flex',
3637
placeItems: 'stretch',
3738
placeContent: 'stretch',
3839
width: {
3940
'': '288px @dialog-size 90vw',
4041
'[data-type="fullscreen"]': '90vw 90vw',
4142
'[data-type="fullscreenTakeover"]': '100vw 100vw',
42-
'[data-type="panel"]': '100vw 100vw',
43+
'[data-type="panel"]': 'auto',
4344
},
4445
height: {
4546
'': 'max 90vh',
4647
'[data-type="fullscreenTakeover"] | [data-type="panel"]': 'max 100vh',
48+
'[data-type="panel"]': 'auto',
4749
},
4850
gap: 0,
4951
flow: 'column',
5052
radius: {
5153
'': '(@large-radius + 1bw)',
5254
'[data-type="tray"]': '(@large-radius + 1bw) top',
53-
'[data-type="fullscreenTakeover"] | [data-type="panel"]': '0r',
55+
'[data-type="fullscreenTakeover"]': '0r',
5456
},
5557
fill: '#white',
5658
shadow: {
57-
'': '0 20px 30px #shadow',
58-
'[data-type="popover"]': '0px 4px 16px #shadow',
59+
'': '0 2x 4x #shadow',
60+
'[data-type="popover"] | [data-type="panel"]': '0px .5x 2x #shadow',
5961
},
6062
top: {
6163
'': false,
6264
'[data-type="modal"]': '((50vh - 50%) / -3)',
65+
'[data-type="panel"]': 'auto',
6366
},
6467
placeSelf: 'stretch',
6568
'@dialog-heading-padding-v': {
@@ -82,10 +85,6 @@ const DialogElement = tasty({
8285
},
8386
});
8487

85-
const StyledFocusLock = styled(FocusLock)`
86-
display: contents;
87-
`;
88-
8988
const CLOSE_BUTTON_STYLES: Styles = {
9089
display: 'flex',
9190
position: 'absolute',
@@ -144,10 +143,16 @@ export const Dialog = forwardRef(function Dialog(
144143

145144
const isEntered = transitionContext?.transitionState === 'entered';
146145

146+
const context = useDialogContext();
147+
148+
const content = useMemo(() => {
149+
return <DialogContent key="content" {...props} ref={ref} />;
150+
}, [props, ref]);
151+
147152
return (
148-
<StyledFocusLock returnFocus disabled={!isEntered}>
149-
<DialogContent key="content" {...props} ref={ref} />
150-
</StyledFocusLock>
153+
<FocusScope restoreFocus contain={isEntered && context.type !== 'panel'}>
154+
{content}
155+
</FocusScope>
151156
);
152157
});
153158

@@ -168,6 +173,8 @@ const DialogContent = forwardRef(function DialogContent(
168173
...otherProps
169174
} = props;
170175

176+
const [isCloseDisabled, setIsCloseDisabled] = useState(true);
177+
171178
size = sizeMap[size.toUpperCase()] || size;
172179

173180
const styles: Styles = extractStyles(otherProps, STYLES_LIST);
@@ -187,6 +194,20 @@ const DialogContent = forwardRef(function DialogContent(
187194
dismissButton = <DismissButton onDismiss={onDismiss} />;
188195
}
189196

197+
const focusManager = useFocusManager();
198+
199+
// Focus the first focusable element in the dialog when it opens
200+
useEffect(() => {
201+
if (contextProps.isOpen) {
202+
setTimeout(() => {
203+
focusManager?.focusFirst();
204+
setIsCloseDisabled(false);
205+
});
206+
} else {
207+
setIsCloseDisabled(true);
208+
}
209+
}, [contextProps.isOpen]);
210+
190211
// let hasHeader = useHasChild('[data-id="Header"]', domRef);
191212
// let hasFooter = useHasChild('[data-id="Footer"]', domRef);
192213

@@ -241,6 +262,7 @@ const DialogContent = forwardRef(function DialogContent(
241262
styles={styles}
242263
as="section"
243264
{...dialogProps}
265+
tabIndex={undefined}
244266
mods={{ dismissable: isDismissable }}
245267
style={{ '--dialog-size': `${sizePxMap[size] || 288}px` }}
246268
data-type={type}
@@ -251,6 +273,7 @@ const DialogContent = forwardRef(function DialogContent(
251273
<SlotProvider slots={slots}>
252274
{isDismissable && (
253275
<Button
276+
isDisabled={isCloseDisabled}
254277
qa="ModalCloseButton"
255278
type="neutral"
256279
styles={CLOSE_BUTTON_STYLES}

src/components/overlays/Dialog/DialogContainer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface CubeDialogContainerProps {
1919
* The type of Dialog that should be rendered. See the visual options below for examples of each.
2020
* @default 'modal'
2121
*/
22-
type?: 'modal' | 'fullscreen' | 'fullscreenTakeover';
22+
type?: 'modal' | 'fullscreen' | 'fullscreenTakeover' | 'panel';
2323
/** Whether the Dialog is dismissible. See the [Dialog docs](Dialog.html#dismissable-dialogs) for more details. */
2424
isDismissable?: boolean;
2525
/**
@@ -64,6 +64,7 @@ export function DialogContainer(props: CubeDialogContainerProps) {
6464
type,
6565
onClose: onDismiss,
6666
isDismissable,
67+
isOpen,
6768
};
6869

6970
return (

src/components/overlays/Dialog/DialogTrigger.tsx

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

13+
import { useCombinedRefs } from '../../../utils/react/index';
1314
import { Modal, Popover, Tray, WithCloseBehavior } from '../Modal';
1415
import { Styles } from '../../../tasty';
1516

@@ -191,6 +192,7 @@ function DialogTrigger(props: CubeDialogTriggerProps) {
191192
isDismissable={isDismissable}
192193
trigger={trigger}
193194
overlay={renderOverlay()}
195+
hideOnClose={hideOnClose}
194196
onClose={onClose}
195197
/>
196198
);
@@ -289,6 +291,8 @@ function PopoverTrigger(allProps) {
289291
}
290292

291293
function DialogTriggerBase(props) {
294+
const ref = useCombinedRefs(props.ref);
295+
292296
let {
293297
type,
294298
state,
@@ -298,18 +302,30 @@ function DialogTriggerBase(props) {
298302
triggerProps = {},
299303
overlay,
300304
trigger,
305+
hideOnClose,
301306
} = props;
302307

303308
let context = {
304309
type,
305310
onClose,
306311
isDismissable,
312+
isOpen: state.isOpen,
307313
...dialogProps,
308314
};
309315

316+
// Restore focus manually when the dialog closes and has `hideOnClose` set to true
317+
useEffect(() => {
318+
if (!state.isOpen && hideOnClose) {
319+
setTimeout(() => {
320+
ref.current?.focus();
321+
});
322+
}
323+
}, [state.isOpen, ref.current]);
324+
310325
return (
311326
<Fragment>
312327
<PressResponder
328+
ref={ref}
313329
{...triggerProps}
314330
isPressed={
315331
state.isOpen &&

src/components/overlays/Dialog/context.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ import { createContext, HTMLAttributes, useContext } from 'react';
22
import invariant from 'tiny-invariant';
33

44
export interface DialogContextValue extends HTMLAttributes<HTMLElement> {
5-
type?: 'modal' | 'popover' | 'tray' | 'fullscreen' | 'fullscreenTakeover';
5+
type?:
6+
| 'modal'
7+
| 'popover'
8+
| 'tray'
9+
| 'fullscreen'
10+
| 'fullscreenTakeover'
11+
| 'panel';
612
isDismissable?: boolean;
713
onClose?: (arg?: string) => void;
14+
isOpen?: boolean;
815
}
916

1017
export const DialogContext = createContext<DialogContextValue | null>({});

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ export default {
3434

3535
const Template: StoryFn<
3636
CubeDialogTriggerProps & { size: CubeDialogProps['size'] }
37-
> = ({ size, ...props }) => {
37+
> = ({ size, styles, ...props }) => {
3838
return (
3939
<DialogTrigger {...props}>
4040
<Button>Click me!</Button>
4141
{(close) => (
42-
<Dialog size={size}>
42+
<Dialog size={size} styles={styles}>
4343
<Header>
4444
<Title>Modal title</Title>
4545
<Text>Header</Text>
@@ -108,6 +108,13 @@ FullscreenTakeover.play = Default.play;
108108
export const Panel: typeof Template = Template.bind({});
109109
Panel.args = {
110110
type: 'panel',
111+
styles: {
112+
right: '2x',
113+
bottom: '2x',
114+
radius: true,
115+
width: '320px',
116+
height: '(100vh - 10x)',
117+
},
111118
};
112119
Panel.play = Default.play;
113120

@@ -191,21 +198,20 @@ CloseOnEsc.play = async (context) => {
191198

192199
await userEvent.click(trigger);
193200

201+
await timeout(500);
202+
194203
const dialog = await findByRole('dialog');
195204

196205
await expect(dialog).toBeInTheDocument();
197206
await expect(dialog.contains(document.activeElement)).toBe(true);
198207

199-
await timeout(500);
200-
201208
await userEvent.keyboard('{Escape}');
202209

203210
await timeout(500);
204211

205212
await expect(dialog).not.toBeInTheDocument();
206213

207-
// @TODO: fix this
208-
// await waitFor(() => expect(document.activeElement).toBe(trigger));
214+
await waitFor(() => expect(document.activeElement).toBe(trigger));
209215
};
210216

211217
export const CloseOnEscCloseBehaviorHide: typeof Template = Template.bind({});
@@ -221,12 +227,12 @@ CloseOnEscCloseBehaviorHide.play = async (context) => {
221227

222228
await userEvent.click(trigger);
223229

230+
await timeout(500);
231+
224232
const dialog = await findByRole('dialog');
225233

226234
await expect(dialog).toBeInTheDocument();
227235

228-
await timeout(500);
229-
230236
await expect(dialog.contains(document.activeElement)).toBe(true);
231237

232238
await userEvent.keyboard('{Escape}');
@@ -235,8 +241,6 @@ CloseOnEscCloseBehaviorHide.play = async (context) => {
235241

236242
await expect(dialog).toBeInTheDocument();
237243

238-
await timeout(500);
239-
240244
await waitFor(() => expect(document.activeElement).toBe(trigger));
241245
};
242246

src/components/overlays/Modal/Modal.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,19 @@ const ModalElement = tasty({
4949
'[data-type="fullscreenTakeover"] | [data-type="panel"]': '100dvh 100dvh',
5050
'[data-type="fullscreen"]': '90dvh 90dvh',
5151
'[data-type="fullscreenTakeover"]': '100dvh 100dvh',
52+
'[data-type="panel"]': 'auto',
5253
},
5354
width: {
54-
width: '288px 90vw',
55+
'': '288px 90vw',
56+
'[data-type="panel"]': 'auto',
5557
},
5658
pointerEvents: 'none',
5759
transition: 'opacity .25s linear, transform .25s ease-in-out',
5860
transform: {
5961
'': 'initial',
6062
'[data-type="modal"] & !open': 'translate(0, -3x) scale(1, 1)',
61-
'[data-type^="fullscreen"] & !open': 'translate(0, 0) scale(1.02, 1.02)',
63+
'([data-type^="fullscreen"] | [data-type="panel"]) & !open':
64+
'translate(0, 0) scale(1.02, 1.02)',
6265
},
6366
opacity: {
6467
'': 0,
@@ -86,7 +89,9 @@ function Modal(props: CubeModalProps, ref) {
8689

8790
return (
8891
<Overlay {...otherProps}>
89-
{type !== 'fullscreenTakeover' && <Underlay {...underlayProps} />}
92+
{type !== 'fullscreenTakeover' && type !== 'panel' && (
93+
<Underlay {...underlayProps} />
94+
)}
9095
<ModalWrapper
9196
ref={domRef}
9297
qa={qa}

0 commit comments

Comments
 (0)