Skip to content

Commit

Permalink
1.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
elite174 committed Jan 2, 2024
1 parent 582ab29 commit 9551480
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 130 deletions.
174 changes: 54 additions & 120 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,40 +26,40 @@ import { flip } from "@floating-ui/dom";

<Popover
// Minimalistic
as="button"
// Awesome typings
type="button"
onClick={() => console.log("button clicked")}
// You'll only see <button>Toggle popover</button> in DOM
trigger={<button id="trigger-button">Toggle popover</button>}
content={<div>This div is visible when popover is open!</div>}
// ------------------------------- The following props are optional
// Only one DOM wrapper over content
contentWrapperClass="content-wrapper-class"
// Full control over position
autoUpdate
computePositionOptions={{ placement: "bottom-start", middleware: [flip()] }}
// Popover API support (where possible)
usePopoverAPI
content={<div>This div is visible when popover is open!</div>}
// Only one DOM wrapper over content
contentWrapperClass="content-wrapper-class"
// Highly customizable
ignoreOutsideInteraction
>
Toggle popover
</Popover>;
dataAttributeName="data-open"
// Astro support! Will work in Astro ignoring astro-slot wrapper
anchorElementSelector="trigger-button"
// SSR support
mount="body"
/>;
```

## Features

- Minimalistic - only one wrapper element for the content!
- Awesome TS support
- Popover API support (with fallback)
- Full control over position
- Highly customizable with imperative API
- Works with SSR and Astro

### Uses only one DOM element to wrap your content

When you render the following code, only `button` (`<button data-expanded="false">Toggle popover!</button>`) will appear in the DOM! No extra DOM nodes. Trigger node will have `data-expanded` attribute, so you can use it in your CSS styles.
When you render the following code, only `button` (`<button data-popover-open="false">Toggle popover!</button>`) will appear in the DOM! No extra DOM nodes. Trigger node will have `data-popover-open` attribute, so you can use it in your CSS styles.

```tsx
<Popover content={<div>Nice content here</div>}>Toggle popover!</Popover>
<Popover trigger={<button>Toggle popover!</button>} content={<div>Nice content here</div>} />
```

When content is visible, it's wrapped with one extra DOM node, but you can control it with the following props:
Expand All @@ -73,20 +73,6 @@ contentWrapperTag?: string;

Also you may use imperative API to get the wrapper element.

## Awesome TS support

By default popover trigger element is button, however it can be anything:

```tsx
// No TS Error!
<Popover as="input" placeholder="Type something" content={<span>hi</span>}></Popover>
```

```tsx
// TS error is here, because button doesn't have `placeholder` attribute
<Popover as="button" placeholder="Type something" content={<span>hi</span>}></Popover>
```

### Popover API support

You can use PopoverAPI! Just pass `usePopoverAPI` prop. Popover will automotically fallback to non-api behavior if popover API is not supported.
Expand All @@ -103,9 +89,7 @@ Don't forget to reset default browser styles for `[popover]`:
```

```tsx
<Popover usePopoverAPI content={<div>Nice content here</div>}>
Toggle popover!
</Popover>
<Popover trigger={<button>Toggle popover!</button>} content={<div>Nice content here</div>} usePopoverAPI />
```

### Full control over position
Expand All @@ -119,130 +103,84 @@ import { flip } from "@floating-ui/dom";
const PositionOptionsExample = () => {
return (
<Popover
defaultOpen
trigger={<button>Toggle popover</button>}
content={<input type="text" />}
defaultOpen
computePositionOptions={{ placement: "bottom-start", middleware: [flip()] }}
autoUpdate
>
click
</Popover>
/>
);
};
```

### Highly customizable with imperative API

It's possible to trigger popover with custom events!
### Works with Astro and SSR

```tsx
import { Popover, type PopoverAPI } from "solid-simple-popover";
import { createEffect, createSignal, onCleanup } from "solid-js";

function App() {
const [open, setOpen] = createSignal(false);
const [poppoverAPI, setPopoverAPI] = createSignal<PopoverAPI>();
// Astro example

createEffect(() => {
const trigger = poppoverAPI()?.getTriggerElement();

const openPopover = () => setOpen(true);
const closePopover = () => setOpen(false);

// You may directly add these listeners to the popover
// thanks to awesome TS support.
// This is an artificial example.
trigger?.addEventListener("focus", openPopover);
trigger?.addEventListener("blur", closePopover);

onCleanup(() => {
trigger?.removeEventListener("focus", openPopover);
trigger?.removeEventListener("blur", closePopover);
});
});

return (
<Popover
open={open()}
as="input"
placeholder="Input some value"
// Don't trigger popover with pointerdown event
triggerEvent={null}
getAPI={setPopoverAPI}
>
<span>hi</span>
</Popover>
);
}
```

The example above is literally this:

```tsx
import { Popover } from "solid-simple-popover";
import { createSignal } from "solid-js";

function App() {
const [open, setOpen] = createSignal(false);

return (
<Popover
open={open()}
as="input"
placeholder="Input some value"
// Don't trigger popover with pointerdown event
triggerEvent={null}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
>
<span>hi</span>
</Popover>
);
}
<Popover
client:idle
anchorElementSelector="#trigger"
mount="body"
computePositionOptions={{ placement: "bottom-start" }}
>
<button id="trigger" slot="trigger">
Toggle popover
</button>
<div slot="content">content</div>
</Popover>
```

## Types

```ts
import { type ComputePositionConfig, type AutoUpdateOptions } from "@floating-ui/dom";
import { type ComponentProps, type JSXElement, type JSX } from "solid-js";
import { type JSXElement, type JSX, type VoidComponent } from "solid-js";

export type PopoverAPI = {
getContentWrapperElement: () => HTMLElement | undefined;
getTriggerElement: () => HTMLElement | undefined;
};

export type PopoverBaseProps<T> = {
children?: JSXElement;
content?: JSXElement;
export type PopoverProps = {
/** HTML Element which triggers popover */
trigger: JSXElement;
content: JSXElement;
open?: boolean;
defaultOpen?: boolean;
/** Should content have the same width as trigger */
sameWidth?: boolean;
/** Options for floating-ui computePosition function */
computePositionOptions?: ComputePositionConfig;
/** @default "button" */
as?: T;
/**
* @default "pointerdown"
* if set to null no event would trigger popover,
* so you need to trigger it mannually with imperative API
* so you need to trigger it mannually
*/
triggerEvent?: string | null;
contentWrapperClass?: string;
contentWrapperStyles?: JSX.CSSProperties;
/** @default "div" */
contentWrapperTag?: string;
/** HTMLElement to mount popover content into */
mount?: HTMLElement;
/**
* HTMLElement or CSS selector (can be used in SSR) to mount popover content into
*/
mount?: HTMLElement | string;
/** Use popover API where possible */
usePopoverAPI?: boolean;
/**
* Ignore outside interaction when popover is open
* By default when popover is open it will listen to "pointerdown" event outside of popover content and trigger
*/
ignoreOutsideInteraction?: boolean;
/**
* Data attribute name to set on trigger element
* @default "data-popover-open"
*/
dataAttributeName?: string;
/**
* CSS selector to find anchor html element inside trigger
* Can be used with Astro, because astro wraps trigger element into astro-slot
* and position breaks
*/
anchorElementSelector?: string;
onOpenChange?: (open: boolean) => void;
getAPI?: (api: PopoverAPI) => void;
setContentWrapperRef?: (wrapperElement: HTMLElement) => void;
} & (
| {
autoUpdate?: false;
Expand All @@ -254,11 +192,7 @@ export type PopoverBaseProps<T> = {
}
);

export type PopoverProps<T extends keyof JSX.IntrinsicElements> = ComponentProps<T> & PopoverBaseProps<T>;

export declare const Popover: <T extends keyof JSX.IntrinsicElements = "button">(
initialProps: PopoverProps<T>
) => JSX.Element;
export declare const Popover: VoidComponent<PopoverProps>;
```

## License
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "solid-simple-popover",
"version": "1.1.0",
"version": "1.2.0",
"description": "A simple popover component for SolidJS",
"author": "Vladislav Lipatov",
"type": "module",
Expand Down
20 changes: 11 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@ import { Popover } from "./lib";

import "./App.css";
import { flip } from "@floating-ui/dom";
import { createEffect, createSignal } from "solid-js";

function App() {
const [contentWrapperRef, setContentWrapperRef] = createSignal<HTMLElement>();

createEffect(() => console.log(contentWrapperRef()));

return (
<Popover
trigger={<button>Toggle popover</button>}
// Minimalistic
// You'll only see <button>Toggle popover</button> in DOM
trigger={<button id="trigger-button">Toggle popover</button>}
content={<div>This div is visible when popover is open!</div>}
// ------------------------------- The following props are optional
// Only one DOM wrapper over content
contentWrapperClass="content-wrapper-class"
// Full control over position
autoUpdate
computePositionOptions={{ placement: "bottom-start", middleware: [flip()] }}
// Popover API support (where possible)
usePopoverAPI
// Only one DOM wrapper over content
contentWrapperClass="content-wrapper-class"
// Highly customizable
ignoreOutsideInteraction
setContentWrapperRef={setContentWrapperRef}
dataAttributeName="data-open"
// Astro support! Will work in Astro ignoring astro-slot wrapper
anchorElementSelector="trigger-button"
// SSR support
mount="body"

/>
);
}
Expand Down

0 comments on commit 9551480

Please sign in to comment.