-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LG-4445: add renderMode and top layer #2482
base: LG-4121-popover-refactor
Are you sure you want to change the base?
Conversation
|
17d4fbc
to
deebe1c
Compare
Size Change: +1.89 kB (+0.14%) Total Size: 1.36 MB
ℹ️ View Unchanged
|
deebe1c
to
557fb83
Compare
cx({ | ||
[css` | ||
const getClosedStyles = (placement: ExtendedPlacement, spacing: number) => { | ||
if (placement.startsWith('top')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think breaking placement
into its constituent align
and justify
, and then using a switch
here would make this a bit more readable, and would likely be easier to test
exited: getClosedStyles(placement, spacing), | ||
entering: getClosedStyles(placement, spacing), | ||
entered: getOpenStyles(placement), | ||
exiting: getClosedStyles(placement, spacing), | ||
unmounted: getClosedStyles(placement, spacing), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed a slight delay after clicking before the popover started transitioning. I wonder if entering
should have getOpenStyles
?
[getTransformStyles(placement, spacing)]: | ||
state === 'exiting' || state === 'entering', | ||
[closedStyles]: state === 'exiting' || state === 'exited', | ||
[openStyles]: state === 'entering' || state === 'entered', | ||
[css` | ||
z-index: ${popoverZIndex}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should z-index have a default?
UseContentNodeReturnObj, | ||
UseReferenceElementReturnObj, | ||
} from './Popover.types'; | ||
|
||
export function usePopoverContextProps( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add some TSDoc
popoverZIndex, | ||
spacing, | ||
...rest | ||
} = props; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we destructure these in the function signature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these Popover.xxx
files should live in Popover/
(except maybe the stories file)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shaneeza What was the initial reasoning for having PopoverContext live in leafygreen-provider
instead of within the popover
package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fwiw, I did try moving PopoverContext
into the popover package while I was consolidating PortalContext
and PopoverContext
. Since the consolidated PopoverProvider
is included in the global LG provider and the @leafygreen-ui/leafygreen-provider
, we can't move PopoverContext
into popover without introducing a circular dependency
export const DismissMode = { | ||
Auto: 'auto', | ||
Manual: 'manual', | ||
} as const; | ||
export type DismissMode = (typeof DismissMode)[keyof typeof DismissMode]; | ||
|
||
/** Local implementation of web-native `ToggleEvent` until we use typescript v5 */ | ||
export interface ToggleEvent extends Event { | ||
type: 'toggle'; | ||
newState: 'open' | 'closed'; | ||
oldState: 'open' | 'closed'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these types should all live in the popover package
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are they duplicated here?
@@ -11,6 +11,39 @@ type TransitionLifecycleCallbacks = Pick< | |||
'onEnter' | 'onEntering' | 'onEntered' | 'onExit' | 'onExiting' | 'onExited' | |||
>; | |||
|
|||
/** | |||
* Options to render the popover element | |||
* @param Inline will render the popover element inline with the reference element |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inline
can have different meanings. Can we be clear that we mean "in the DOM where it's written" and not "display: inline`
* @param Portal will render the popover element in a provided `portalContainer` or | ||
* in a new div appended to the body |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: should lead with the default case (in a div appended to the body)
export type DismissMode = (typeof DismissMode)[keyof typeof DismissMode]; | ||
|
||
/** Local implementation of web-native `ToggleEvent` until we use typescript v5 */ | ||
export interface ToggleEvent extends Event { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this extends Event
, does it? (i.e. it doesn't have e.target
etc. iirc)
*/ | ||
scrollContainer?: null; | ||
}; | ||
/** @deprecated - use {@link RenderTopLayerProps} */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could potentially add more on why we recommend TopLayerProps
export type PopoverRenderModeProps = | ||
| RenderInlineProps | ||
| RenderPortalProps | ||
| RenderTopLayerProps; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe the deprecated
flags above will be seen by consumers, since we're grouping them into this type here
setIsPopoverOpen, | ||
} = usePopoverContext(); | ||
...restProps | ||
} = usePopoverContextProps(rest, popoverContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this need to be a hook?
/** | ||
* When usePortal is true and a scrollContainer is defined, log a warning if the | ||
* portalContainer is not inside of the scrollContainer. | ||
* Note: If no portalContainer is passed the portalContainer will be undefined, and | ||
* this warning will show up. By default if no portalContainer is passed the <Portal> | ||
* component will create a div and append it to the body. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
/**
* When `usePortal` is true and a `scrollContainer` is defined,
* log a warning if the `portalContainer` is not inside of the `scrollContainer`.
*
* Note: If no `portalContainer` is provided,
* the `Portal` component will create a `div` and append it to the body.
*/
@@ -190,16 +231,24 @@ export const Popover = forwardRef<HTMLDivElement, PopoverComponentProps>( | |||
)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we still need this placeholder?
{/* We need to put `setContentNode` ref on this inner wrapper because | ||
placing the ref on the parent will create an infinite loop in some cases |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to use setContentNode
? Can we use a standard useRef
RefObject?
if (context.open) { | ||
// @ts-expect-error - Popover API not currently supported in react v18 https://github.com/facebook/react/pull/27981 | ||
refs.floating.current?.showPopover?.(); | ||
} else { | ||
// @ts-expect-error - Popover API not currently supported in react v18 https://github.com/facebook/react/pull/27981 | ||
refs.floating.current?.hidePopover?.(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: might want to check whether context.open
has changed, otherwise we'll be calling show/hide unnecessarily. Shouldn't be a big deal but idk how this API works under the hood
refs.floating.current?.addEventListener('toggle', onToggle); | ||
return () => | ||
// @ts-expect-error - `toggle` event not supported pre-typescript v5 | ||
refs.floating.current?.removeEventListener('toggle', onToggle); | ||
}, [onToggle, renderMode]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like we're updating the event listener on each toggle
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(See below comment)
useEffect(() => { | ||
if (renderMode !== RenderMode.TopLayer || !onToggle) { | ||
return; | ||
} | ||
|
||
// @ts-expect-error - `toggle` event not supported pre-typescript v5 | ||
refs.floating.current?.addEventListener('toggle', onToggle); | ||
return () => | ||
// @ts-expect-error - `toggle` event not supported pre-typescript v5 | ||
refs.floating.current?.removeEventListener('toggle', onToggle); | ||
}, [onToggle, renderMode]); | ||
|
||
useEffect(() => { | ||
if (renderMode !== RenderMode.TopLayer) { | ||
return; | ||
} | ||
|
||
if (context.open) { | ||
// @ts-expect-error - Popover API not currently supported in react v18 https://github.com/facebook/react/pull/27981 | ||
refs.floating.current?.showPopover?.(); | ||
} else { | ||
// @ts-expect-error - Popover API not currently supported in react v18 https://github.com/facebook/react/pull/27981 | ||
refs.floating.current?.hidePopover?.(); | ||
} | ||
}, [context.open, renderMode]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like each time the toggle button is clicked, the Popover fully unmounts and re-mounts. This first effect is fired on every toggle, but the second is never fired
<Instance | ||
buttonText={undefined} | ||
className={css` | ||
background-color: ${color.light.background.primary.default}; | ||
`} | ||
dismissMode="manual" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember what the answer was last time: does this need to be inside the Button?
dismissMode="manual" | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be an arg
@@ -111,23 +118,28 @@ const meta: StoryMetaType<typeof Popover> = { | |||
}, | |||
// eslint-disable-next-line react/display-name | |||
decorator: Instance => { | |||
// eslint-disable-next-line react-hooks/rules-of-hooks | |||
const [active, setActive] = useState<boolean>(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how is this working?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see this is just the generated decorator?
can we do
const [_, setActive] = useState<boolean>(false);
or just cast a void function as a click handler to Button?
<Popover | ||
{...rest} | ||
active={active} | ||
renderMode={RenderMode.TopLayer} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
technically we should probably open up the renderMode
as a control in the LiveExample
RenderModePortalInScrollableContainer.parameters = { | ||
chromatic: { | ||
disableSnapshot: true, | ||
}, | ||
}; | ||
ScrollableContainer.args = { | ||
usePortal: true, | ||
RenderModePortalInScrollableContainer.argTypes = { | ||
renderMode: { control: 'none' }, | ||
portalClassName: { control: 'none' }, | ||
refEl: { control: 'none' }, | ||
className: { control: 'none' }, | ||
active: { control: 'none' }, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, could probably move to StoryObj for the rest of these stories as well
✍️ Proposed changes
renderMode
,dismissMode
, andonToggle
props toPopover
componentusePortal
currently overridesrenderMode
prop but will be dropped in an upcoming PRrenderMode="top-layer"
🎟 Jira ticket: LG-4445
✅ Checklist
For bug fixes, new features & breaking changes
yarn changeset
and documented my changes🧪 How to test changes
Popover
storiesCombobox/LiveExample
DatePicker/LiveExample
GuideCue/LiveExample
NumberInput/LiveExample
SearchInput/LiveExample
Chat/InputBar/WithDropdown