-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathDialog.tsx
More file actions
160 lines (145 loc) · 6.02 KB
/
Dialog.tsx
File metadata and controls
160 lines (145 loc) · 6.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/*
* Copyright 2022 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {AriaDialogProps, useDialog, useId, useOverlayTrigger} from 'react-aria';
import {ButtonContext} from './Button';
import {ContextValue, DEFAULT_SLOT, dom, DOMRenderProps, Provider, SlotProps, StyleProps, useContextProps, useRenderProps} from './utils';
import {filterDOMProps, mergeProps, useResizeObserver} from '@react-aria/utils';
import {forwardRefType, GlobalDOMAttributes} from '@react-types/shared';
import {HeadingContext} from './RSPContexts';
import {OverlayTriggerProps, OverlayTriggerState, useMenuTriggerState} from 'react-stately';
import {PopoverContext} from './Popover';
import {PressResponder} from '@react-aria/interactions';
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useCallback, useContext, useRef, useState} from 'react';
import {RootMenuTriggerStateContext} from './Menu';
export interface DialogTriggerProps extends OverlayTriggerProps {
children: ReactNode
}
export interface DialogRenderProps {
close: () => void
}
export interface DialogProps extends AriaDialogProps, StyleProps, SlotProps, DOMRenderProps<'section', undefined>, GlobalDOMAttributes<HTMLElement> {
/**
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element.
* @default 'react-aria-Dialog'
*/
className?: string,
/** Children of the dialog. A function may be provided to access a function to close the dialog. */
children?: ReactNode | ((opts: DialogRenderProps) => ReactNode)
}
export const DialogContext = createContext<ContextValue<DialogProps, HTMLElement>>(null);
export const OverlayTriggerStateContext = createContext<OverlayTriggerState | null>(null);
/**
* A DialogTrigger opens a dialog when a trigger element is pressed.
*/
export function DialogTrigger(props: DialogTriggerProps): JSX.Element {
// Use useMenuTriggerState instead of useOverlayTriggerState in case a menu is embedded in the dialog.
// This is needed to handle submenus.
let state = useMenuTriggerState(props);
let buttonRef = useRef<HTMLButtonElement>(null);
let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, buttonRef);
// Allows popover width to match trigger element
let [buttonWidth, setButtonWidth] = useState<string | null>(null);
let onResize = useCallback(() => {
if (buttonRef.current) {
setButtonWidth(buttonRef.current.offsetWidth + 'px');
}
}, [buttonRef]);
useResizeObserver({
ref: buttonRef,
onResize: onResize
});
// Label dialog by the trigger as a fallback if there is no title slot.
// This is done in RAC instead of hooks because otherwise we cannot distinguish
// between context and props. Normally aria-labelledby overrides the title
// but when sent by context we want the title to win.
triggerProps.id = useId();
overlayProps['aria-labelledby'] = triggerProps.id;
return (
<Provider
values={[
[OverlayTriggerStateContext, state],
[RootMenuTriggerStateContext, state],
[DialogContext, overlayProps],
[PopoverContext, {
trigger: 'DialogTrigger',
triggerRef: buttonRef,
id: overlayProps.id,
'aria-labelledby': overlayProps['aria-labelledby'],
style: {'--trigger-width': buttonWidth} as React.CSSProperties
}]
]}>
<PressResponder {...triggerProps} ref={buttonRef} isPressed={state.isOpen}>
{props.children}
</PressResponder>
</Provider>
);
}
/**
* A dialog is an overlay shown above other content in an application.
*/
export const Dialog = /*#__PURE__*/ (forwardRef as forwardRefType)(function Dialog(props: DialogProps, ref: ForwardedRef<HTMLElement>) {
let originalAriaLabelledby = props['aria-labelledby'];
[props, ref] = useContextProps(props, ref, DialogContext);
let {dialogProps, titleProps} = useDialog({
...props,
// Only pass aria-labelledby from props, not context.
// Context is used as a fallback below.
'aria-labelledby': originalAriaLabelledby
}, ref);
let state = useContext(OverlayTriggerStateContext);
if (!dialogProps['aria-label'] && !dialogProps['aria-labelledby']) {
// If aria-labelledby exists on props, we know it came from context.
// Use that as a fallback in case there is no title slot.
if (props['aria-labelledby']) {
dialogProps['aria-labelledby'] = props['aria-labelledby'];
} else if (process.env.NODE_ENV !== 'production') {
console.warn('If a Dialog does not contain a <Heading slot="title">, it must have an aria-label or aria-labelledby attribute for accessibility.');
}
}
let renderProps = useRenderProps({
defaultClassName: 'react-aria-Dialog',
className: props.className,
style: props.style,
children: props.children,
values: {
close: state?.close || (() => {})
}
});
let DOMProps = filterDOMProps(props, {global: true});
return (
<dom.section
{...mergeProps(DOMProps, renderProps, dialogProps)}
render={props.render}
ref={ref}
slot={props.slot || undefined}>
<Provider
values={[
[HeadingContext, {
slots: {
[DEFAULT_SLOT]: {},
title: {...titleProps, level: 2}
}
}],
[ButtonContext, {
slots: {
[DEFAULT_SLOT]: {},
close: {
onPress: () => state?.close()
}
}
}]
]}>
{renderProps.children}
</Provider>
</dom.section>
);
});