@nepdatejs/dom 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +10 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/position.ts +13 -2
package/dist/index.cjs
CHANGED
|
@@ -95,14 +95,22 @@ function hider(panel, topLayer) {
|
|
|
95
95
|
function trackPosition(anchor, panel, options = {}) {
|
|
96
96
|
if (!hasDom()) return noop$1;
|
|
97
97
|
let hide = placePopover(anchor, panel, options);
|
|
98
|
+
let frame = null;
|
|
98
99
|
const place = () => {
|
|
99
100
|
hide = placePopover(anchor, panel, options);
|
|
100
101
|
};
|
|
102
|
+
const placeOnNextFrame = () => {
|
|
103
|
+
frame ?? (frame = requestAnimationFrame(() => {
|
|
104
|
+
frame = null;
|
|
105
|
+
place();
|
|
106
|
+
}));
|
|
107
|
+
};
|
|
101
108
|
window.addEventListener("resize", place);
|
|
102
|
-
window.addEventListener("scroll",
|
|
109
|
+
window.addEventListener("scroll", placeOnNextFrame, true);
|
|
103
110
|
return () => {
|
|
104
111
|
window.removeEventListener("resize", place);
|
|
105
|
-
window.removeEventListener("scroll",
|
|
112
|
+
window.removeEventListener("scroll", placeOnNextFrame, true);
|
|
113
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
106
114
|
hide();
|
|
107
115
|
};
|
|
108
116
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["noop"],"sources":["../src/env.ts","../src/position.ts","../src/events.ts"],"sourcesContent":["/** True in a browser-like environment; false during server rendering. */\nexport function hasDom(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/** Whether the browser can show elements in the top layer (`popover` + `showPopover`). */\nexport function supportsPopover(): boolean {\n return (\n hasDom() &&\n typeof HTMLElement !== 'undefined' &&\n typeof HTMLElement.prototype.showPopover === 'function'\n );\n}\n\n/** Whether the user asked the system for less motion. */\nexport function prefersReducedMotion(): boolean {\n return (\n hasDom() &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n}\n","import { hasDom, supportsPopover } from './env';\n\nexport type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';\n\n/** 'popover' sits beside the anchor; 'sheet' is a bottom sheet; 'auto' picks the sheet on narrow screens. */\nexport type PanelLayout = 'auto' | 'popover' | 'sheet';\n\nexport interface PlaceOptions {\n /** Space between the anchor and the panel, and kept from the viewport edges. Default 6. */\n readonly gap?: number | undefined;\n /** Panel width in px; shrinks to fit narrow screens. Default 320. */\n readonly width?: number | undefined;\n /** Preferred side; flips when there is not enough room. Default 'bottom-start'. */\n readonly placement?: Placement | undefined;\n /** Default 'auto'. */\n readonly layout?: PanelLayout | undefined;\n /** 'auto' uses the sheet below this viewport width, in px. Default 480. */\n readonly sheetBelow?: number | undefined;\n}\n\n/** The layout to use for a viewport width. Pure. */\nexport function resolveLayout(\n layout: PanelLayout | undefined,\n viewportWidth: number,\n sheetBelow = 480,\n): 'popover' | 'sheet' {\n if (layout !== undefined && layout !== 'auto') {\n return layout;\n }\n\n return viewportWidth < sheetBelow ? 'sheet' : 'popover';\n}\n\nexport interface Rect {\n readonly top: number;\n readonly left: number;\n readonly bottom: number;\n readonly right: number;\n}\n\nexport interface Position {\n readonly top: number;\n readonly left: number;\n readonly width: number;\n /** Where the panel ended up after flipping. */\n readonly side: 'top' | 'bottom';\n}\n\n/**\n * Where to put a panel next to an anchor, in viewport (position: fixed)\n * coordinates. Pure, so it can be tested without a browser. Rules from the\n * institute-SaaS picker: prefer the requested side, flip when the panel does\n * not fit but does fit on the other side, and keep `gap` px from every edge\n * (the panel may overlap the anchor rather than leave the screen).\n */\nexport function computePosition(\n anchor: Rect,\n panelHeight: number,\n viewport: { readonly width: number; readonly height: number },\n options: PlaceOptions = {},\n): Position {\n const gap = options.gap ?? 6;\n const placement = options.placement ?? 'bottom-start';\n const width = Math.max(0, Math.min(options.width ?? 320, viewport.width - gap * 2));\n const roomBelow = viewport.height - anchor.bottom - gap * 2;\n const roomAbove = anchor.top - gap * 2;\n const wantsTop = placement.startsWith('top');\n const side: 'top' | 'bottom' = wantsTop\n ? roomAbove >= panelHeight || roomAbove >= roomBelow\n ? 'top'\n : 'bottom'\n : roomBelow >= panelHeight || roomBelow >= roomAbove\n ? 'bottom'\n : 'top';\n const rawTop = side === 'bottom' ? anchor.bottom + gap : anchor.top - gap - panelHeight;\n const rawLeft = placement.endsWith('end') ? anchor.right - width : anchor.left;\n\n return {\n // Keep the whole panel on screen; when it is taller than the room, its top edge wins.\n top: Math.max(gap, Math.min(rawTop, viewport.height - gap - panelHeight)),\n left: Math.max(gap, Math.min(rawLeft, viewport.width - width - gap)),\n width,\n side,\n };\n}\n\n/**\n * Show `panel` beside `anchor` once. Uses the browser's top layer when it can\n * (so dialogs and scrolling containers never clip the panel), otherwise\n * position: fixed. Returns a function that hides the panel again.\n */\nexport function placePopover(\n anchor: Element,\n panel: HTMLElement,\n options: PlaceOptions = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const topLayer = supportsPopover();\n if (topLayer) {\n if (!panel.hasAttribute('popover')) {\n panel.setAttribute('popover', 'manual');\n }\n if (!panel.matches(':popover-open')) {\n panel.showPopover();\n }\n }\n const layout = resolveLayout(options.layout, window.innerWidth, options.sheetBelow);\n panel.dataset['layout'] = layout;\n if (layout === 'sheet') {\n // The stylesheet pins a sheet to the bottom edge; clear any popover coordinates.\n Object.assign(panel.style, {\n top: '',\n left: '',\n width: '',\n position: 'fixed',\n margin: '0',\n inset: 'auto 0 0 0',\n });\n delete panel.dataset['side'];\n\n return hider(panel, topLayer);\n }\n const position = computePosition(\n anchor.getBoundingClientRect(),\n panel.offsetHeight,\n { width: window.innerWidth, height: window.innerHeight },\n options,\n );\n Object.assign(panel.style, {\n position: 'fixed',\n margin: '0',\n inset: 'auto',\n top: `${position.top}px`,\n left: `${position.left}px`,\n width: `${position.width}px`,\n });\n panel.dataset['side'] = position.side;\n\n return hider(panel, topLayer);\n}\n\nfunction hider(panel: HTMLElement, topLayer: boolean): () => void {\n return () => {\n if (topLayer && panel.matches(':popover-open')) {\n panel.hidePopover();\n }\n };\n}\n\n/**\n * Show the panel and keep it beside the anchor while the window resizes or\n * any ancestor scrolls. Returns a function that stops tracking and hides it.\n */\nexport function trackPosition(\n anchor: Element,\n panel: HTMLElement,\n options: PlaceOptions = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n let hide = placePopover(anchor, panel, options);\n const place = (): void => {\n hide = placePopover(anchor, panel, options);\n };\n window.addEventListener('resize', place);\n window.addEventListener('scroll', place, true);\n\n return () => {\n window.removeEventListener('resize', place);\n window.removeEventListener('scroll', place, true);\n hide();\n };\n}\n\nfunction noop(): void {\n // Nothing to undo outside a browser.\n}\n","import { hasDom } from './env';\n\ntype Targets =\n | readonly (Element | null | undefined)[]\n | (() => readonly (Element | null | undefined)[]);\n\n/**\n * Call `callback` when a pointer goes down outside all of `elements` (checked\n * in the capture phase, before the page reacts). Returns a cleanup function.\n */\nexport function onOutsidePointer(\n elements: Targets,\n callback: (event: PointerEvent) => void,\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const listener = (event: PointerEvent): void => {\n const target = event.target;\n const list = typeof elements === 'function' ? elements() : elements;\n const inside =\n target instanceof Node &&\n list.some((element) => element?.contains(target) ?? false);\n if (!inside) {\n callback(event);\n }\n };\n document.addEventListener('pointerdown', listener, true);\n\n return () => {\n document.removeEventListener('pointerdown', listener, true);\n };\n}\n\n/**\n * Call `callback` when the pointer goes down on a panel's backdrop (the dimmed\n * area around a bottom sheet). A top-layer panel receives those events itself,\n * so they are told apart by position: on the panel, but outside its box.\n */\nexport function onBackdropPointer(\n panel: HTMLElement,\n callback: (event: PointerEvent) => void,\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const listener = (event: PointerEvent): void => {\n if (event.target !== panel) {\n return;\n }\n const box = panel.getBoundingClientRect();\n const outside =\n event.clientX < box.left ||\n event.clientX > box.right ||\n event.clientY < box.top ||\n event.clientY > box.bottom;\n if (outside) {\n callback(event);\n }\n };\n panel.addEventListener('pointerdown', listener);\n\n return () => {\n panel.removeEventListener('pointerdown', listener);\n };\n}\n\n/**\n * Call `callback` on Escape. By default it listens in the capture phase and\n * stops the event, so an enclosing dialog does not close as well.\n */\nexport function onEscape(\n callback: (event: KeyboardEvent) => void,\n options: { readonly capture?: boolean; readonly stop?: boolean } = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const capture = options.capture ?? true;\n const listener = (event: KeyboardEvent): void => {\n if (event.key !== 'Escape') {\n return;\n }\n if (options.stop ?? true) {\n event.stopPropagation();\n event.preventDefault();\n }\n callback(event);\n };\n window.addEventListener('keydown', listener, capture);\n\n return () => {\n window.removeEventListener('keydown', listener, capture);\n };\n}\n\n/** Focus the first element in `container` matching `selector` (the roving-tabindex day by default). */\nexport function focusFirst(\n container: ParentNode | null,\n selector = '[tabindex=\"0\"]',\n): boolean {\n const element = container?.querySelector<HTMLElement>(selector);\n if (!element) {\n return false;\n }\n element.focus();\n\n return true;\n}\n\n/** Put focus back, e.g. on the input after the panel closes. */\nexport function restoreFocus(element: HTMLElement | null | undefined): void {\n element?.focus({ preventScroll: true });\n}\n\nfunction noop(): void {\n // Nothing to undo outside a browser.\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["noop"],"sources":["../src/env.ts","../src/position.ts","../src/events.ts"],"sourcesContent":["/** True in a browser-like environment; false during server rendering. */\nexport function hasDom(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/** Whether the browser can show elements in the top layer (`popover` + `showPopover`). */\nexport function supportsPopover(): boolean {\n return (\n hasDom() &&\n typeof HTMLElement !== 'undefined' &&\n typeof HTMLElement.prototype.showPopover === 'function'\n );\n}\n\n/** Whether the user asked the system for less motion. */\nexport function prefersReducedMotion(): boolean {\n return (\n hasDom() &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n}\n","import { hasDom, supportsPopover } from './env';\n\nexport type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';\n\n/** 'popover' sits beside the anchor; 'sheet' is a bottom sheet; 'auto' picks the sheet on narrow screens. */\nexport type PanelLayout = 'auto' | 'popover' | 'sheet';\n\nexport interface PlaceOptions {\n /** Space between the anchor and the panel, and kept from the viewport edges. Default 6. */\n readonly gap?: number | undefined;\n /** Panel width in px; shrinks to fit narrow screens. Default 320. */\n readonly width?: number | undefined;\n /** Preferred side; flips when there is not enough room. Default 'bottom-start'. */\n readonly placement?: Placement | undefined;\n /** Default 'auto'. */\n readonly layout?: PanelLayout | undefined;\n /** 'auto' uses the sheet below this viewport width, in px. Default 480. */\n readonly sheetBelow?: number | undefined;\n}\n\n/** The layout to use for a viewport width. Pure. */\nexport function resolveLayout(\n layout: PanelLayout | undefined,\n viewportWidth: number,\n sheetBelow = 480,\n): 'popover' | 'sheet' {\n if (layout !== undefined && layout !== 'auto') {\n return layout;\n }\n\n return viewportWidth < sheetBelow ? 'sheet' : 'popover';\n}\n\nexport interface Rect {\n readonly top: number;\n readonly left: number;\n readonly bottom: number;\n readonly right: number;\n}\n\nexport interface Position {\n readonly top: number;\n readonly left: number;\n readonly width: number;\n /** Where the panel ended up after flipping. */\n readonly side: 'top' | 'bottom';\n}\n\n/**\n * Where to put a panel next to an anchor, in viewport (position: fixed)\n * coordinates. Pure, so it can be tested without a browser. Rules from the\n * institute-SaaS picker: prefer the requested side, flip when the panel does\n * not fit but does fit on the other side, and keep `gap` px from every edge\n * (the panel may overlap the anchor rather than leave the screen).\n */\nexport function computePosition(\n anchor: Rect,\n panelHeight: number,\n viewport: { readonly width: number; readonly height: number },\n options: PlaceOptions = {},\n): Position {\n const gap = options.gap ?? 6;\n const placement = options.placement ?? 'bottom-start';\n const width = Math.max(0, Math.min(options.width ?? 320, viewport.width - gap * 2));\n const roomBelow = viewport.height - anchor.bottom - gap * 2;\n const roomAbove = anchor.top - gap * 2;\n const wantsTop = placement.startsWith('top');\n const side: 'top' | 'bottom' = wantsTop\n ? roomAbove >= panelHeight || roomAbove >= roomBelow\n ? 'top'\n : 'bottom'\n : roomBelow >= panelHeight || roomBelow >= roomAbove\n ? 'bottom'\n : 'top';\n const rawTop = side === 'bottom' ? anchor.bottom + gap : anchor.top - gap - panelHeight;\n const rawLeft = placement.endsWith('end') ? anchor.right - width : anchor.left;\n\n return {\n // Keep the whole panel on screen; when it is taller than the room, its top edge wins.\n top: Math.max(gap, Math.min(rawTop, viewport.height - gap - panelHeight)),\n left: Math.max(gap, Math.min(rawLeft, viewport.width - width - gap)),\n width,\n side,\n };\n}\n\n/**\n * Show `panel` beside `anchor` once. Uses the browser's top layer when it can\n * (so dialogs and scrolling containers never clip the panel), otherwise\n * position: fixed. Returns a function that hides the panel again.\n */\nexport function placePopover(\n anchor: Element,\n panel: HTMLElement,\n options: PlaceOptions = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const topLayer = supportsPopover();\n if (topLayer) {\n if (!panel.hasAttribute('popover')) {\n panel.setAttribute('popover', 'manual');\n }\n if (!panel.matches(':popover-open')) {\n panel.showPopover();\n }\n }\n const layout = resolveLayout(options.layout, window.innerWidth, options.sheetBelow);\n panel.dataset['layout'] = layout;\n if (layout === 'sheet') {\n // The stylesheet pins a sheet to the bottom edge; clear any popover coordinates.\n Object.assign(panel.style, {\n top: '',\n left: '',\n width: '',\n position: 'fixed',\n margin: '0',\n inset: 'auto 0 0 0',\n });\n delete panel.dataset['side'];\n\n return hider(panel, topLayer);\n }\n const position = computePosition(\n anchor.getBoundingClientRect(),\n panel.offsetHeight,\n { width: window.innerWidth, height: window.innerHeight },\n options,\n );\n Object.assign(panel.style, {\n position: 'fixed',\n margin: '0',\n inset: 'auto',\n top: `${position.top}px`,\n left: `${position.left}px`,\n width: `${position.width}px`,\n });\n panel.dataset['side'] = position.side;\n\n return hider(panel, topLayer);\n}\n\nfunction hider(panel: HTMLElement, topLayer: boolean): () => void {\n return () => {\n if (topLayer && panel.matches(':popover-open')) {\n panel.hidePopover();\n }\n };\n}\n\n/**\n * Show the panel and keep it beside the anchor while the window resizes or\n * any ancestor scrolls. Returns a function that stops tracking and hides it.\n */\nexport function trackPosition(\n anchor: Element,\n panel: HTMLElement,\n options: PlaceOptions = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n let hide = placePopover(anchor, panel, options);\n let frame: number | null = null;\n const place = (): void => {\n hide = placePopover(anchor, panel, options);\n };\n // Scroll events can outnumber frames, and each placement measures the page.\n const placeOnNextFrame = (): void => {\n frame ??= requestAnimationFrame(() => {\n frame = null;\n place();\n });\n };\n window.addEventListener('resize', place);\n window.addEventListener('scroll', placeOnNextFrame, true);\n\n return () => {\n window.removeEventListener('resize', place);\n window.removeEventListener('scroll', placeOnNextFrame, true);\n if (frame !== null) {\n cancelAnimationFrame(frame);\n }\n hide();\n };\n}\n\nfunction noop(): void {\n // Nothing to undo outside a browser.\n}\n","import { hasDom } from './env';\n\ntype Targets =\n | readonly (Element | null | undefined)[]\n | (() => readonly (Element | null | undefined)[]);\n\n/**\n * Call `callback` when a pointer goes down outside all of `elements` (checked\n * in the capture phase, before the page reacts). Returns a cleanup function.\n */\nexport function onOutsidePointer(\n elements: Targets,\n callback: (event: PointerEvent) => void,\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const listener = (event: PointerEvent): void => {\n const target = event.target;\n const list = typeof elements === 'function' ? elements() : elements;\n const inside =\n target instanceof Node &&\n list.some((element) => element?.contains(target) ?? false);\n if (!inside) {\n callback(event);\n }\n };\n document.addEventListener('pointerdown', listener, true);\n\n return () => {\n document.removeEventListener('pointerdown', listener, true);\n };\n}\n\n/**\n * Call `callback` when the pointer goes down on a panel's backdrop (the dimmed\n * area around a bottom sheet). A top-layer panel receives those events itself,\n * so they are told apart by position: on the panel, but outside its box.\n */\nexport function onBackdropPointer(\n panel: HTMLElement,\n callback: (event: PointerEvent) => void,\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const listener = (event: PointerEvent): void => {\n if (event.target !== panel) {\n return;\n }\n const box = panel.getBoundingClientRect();\n const outside =\n event.clientX < box.left ||\n event.clientX > box.right ||\n event.clientY < box.top ||\n event.clientY > box.bottom;\n if (outside) {\n callback(event);\n }\n };\n panel.addEventListener('pointerdown', listener);\n\n return () => {\n panel.removeEventListener('pointerdown', listener);\n };\n}\n\n/**\n * Call `callback` on Escape. By default it listens in the capture phase and\n * stops the event, so an enclosing dialog does not close as well.\n */\nexport function onEscape(\n callback: (event: KeyboardEvent) => void,\n options: { readonly capture?: boolean; readonly stop?: boolean } = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const capture = options.capture ?? true;\n const listener = (event: KeyboardEvent): void => {\n if (event.key !== 'Escape') {\n return;\n }\n if (options.stop ?? true) {\n event.stopPropagation();\n event.preventDefault();\n }\n callback(event);\n };\n window.addEventListener('keydown', listener, capture);\n\n return () => {\n window.removeEventListener('keydown', listener, capture);\n };\n}\n\n/** Focus the first element in `container` matching `selector` (the roving-tabindex day by default). */\nexport function focusFirst(\n container: ParentNode | null,\n selector = '[tabindex=\"0\"]',\n): boolean {\n const element = container?.querySelector<HTMLElement>(selector);\n if (!element) {\n return false;\n }\n element.focus();\n\n return true;\n}\n\n/** Put focus back, e.g. on the input after the panel closes. */\nexport function restoreFocus(element: HTMLElement | null | undefined): void {\n element?.focus({ preventScroll: true });\n}\n\nfunction noop(): void {\n // Nothing to undo outside a browser.\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -94,14 +94,22 @@ function hider(panel, topLayer) {
|
|
|
94
94
|
function trackPosition(anchor, panel, options = {}) {
|
|
95
95
|
if (!hasDom()) return noop$1;
|
|
96
96
|
let hide = placePopover(anchor, panel, options);
|
|
97
|
+
let frame = null;
|
|
97
98
|
const place = () => {
|
|
98
99
|
hide = placePopover(anchor, panel, options);
|
|
99
100
|
};
|
|
101
|
+
const placeOnNextFrame = () => {
|
|
102
|
+
frame ?? (frame = requestAnimationFrame(() => {
|
|
103
|
+
frame = null;
|
|
104
|
+
place();
|
|
105
|
+
}));
|
|
106
|
+
};
|
|
100
107
|
window.addEventListener("resize", place);
|
|
101
|
-
window.addEventListener("scroll",
|
|
108
|
+
window.addEventListener("scroll", placeOnNextFrame, true);
|
|
102
109
|
return () => {
|
|
103
110
|
window.removeEventListener("resize", place);
|
|
104
|
-
window.removeEventListener("scroll",
|
|
111
|
+
window.removeEventListener("scroll", placeOnNextFrame, true);
|
|
112
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
105
113
|
hide();
|
|
106
114
|
};
|
|
107
115
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["noop"],"sources":["../src/env.ts","../src/position.ts","../src/events.ts"],"sourcesContent":["/** True in a browser-like environment; false during server rendering. */\nexport function hasDom(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/** Whether the browser can show elements in the top layer (`popover` + `showPopover`). */\nexport function supportsPopover(): boolean {\n return (\n hasDom() &&\n typeof HTMLElement !== 'undefined' &&\n typeof HTMLElement.prototype.showPopover === 'function'\n );\n}\n\n/** Whether the user asked the system for less motion. */\nexport function prefersReducedMotion(): boolean {\n return (\n hasDom() &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n}\n","import { hasDom, supportsPopover } from './env';\n\nexport type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';\n\n/** 'popover' sits beside the anchor; 'sheet' is a bottom sheet; 'auto' picks the sheet on narrow screens. */\nexport type PanelLayout = 'auto' | 'popover' | 'sheet';\n\nexport interface PlaceOptions {\n /** Space between the anchor and the panel, and kept from the viewport edges. Default 6. */\n readonly gap?: number | undefined;\n /** Panel width in px; shrinks to fit narrow screens. Default 320. */\n readonly width?: number | undefined;\n /** Preferred side; flips when there is not enough room. Default 'bottom-start'. */\n readonly placement?: Placement | undefined;\n /** Default 'auto'. */\n readonly layout?: PanelLayout | undefined;\n /** 'auto' uses the sheet below this viewport width, in px. Default 480. */\n readonly sheetBelow?: number | undefined;\n}\n\n/** The layout to use for a viewport width. Pure. */\nexport function resolveLayout(\n layout: PanelLayout | undefined,\n viewportWidth: number,\n sheetBelow = 480,\n): 'popover' | 'sheet' {\n if (layout !== undefined && layout !== 'auto') {\n return layout;\n }\n\n return viewportWidth < sheetBelow ? 'sheet' : 'popover';\n}\n\nexport interface Rect {\n readonly top: number;\n readonly left: number;\n readonly bottom: number;\n readonly right: number;\n}\n\nexport interface Position {\n readonly top: number;\n readonly left: number;\n readonly width: number;\n /** Where the panel ended up after flipping. */\n readonly side: 'top' | 'bottom';\n}\n\n/**\n * Where to put a panel next to an anchor, in viewport (position: fixed)\n * coordinates. Pure, so it can be tested without a browser. Rules from the\n * institute-SaaS picker: prefer the requested side, flip when the panel does\n * not fit but does fit on the other side, and keep `gap` px from every edge\n * (the panel may overlap the anchor rather than leave the screen).\n */\nexport function computePosition(\n anchor: Rect,\n panelHeight: number,\n viewport: { readonly width: number; readonly height: number },\n options: PlaceOptions = {},\n): Position {\n const gap = options.gap ?? 6;\n const placement = options.placement ?? 'bottom-start';\n const width = Math.max(0, Math.min(options.width ?? 320, viewport.width - gap * 2));\n const roomBelow = viewport.height - anchor.bottom - gap * 2;\n const roomAbove = anchor.top - gap * 2;\n const wantsTop = placement.startsWith('top');\n const side: 'top' | 'bottom' = wantsTop\n ? roomAbove >= panelHeight || roomAbove >= roomBelow\n ? 'top'\n : 'bottom'\n : roomBelow >= panelHeight || roomBelow >= roomAbove\n ? 'bottom'\n : 'top';\n const rawTop = side === 'bottom' ? anchor.bottom + gap : anchor.top - gap - panelHeight;\n const rawLeft = placement.endsWith('end') ? anchor.right - width : anchor.left;\n\n return {\n // Keep the whole panel on screen; when it is taller than the room, its top edge wins.\n top: Math.max(gap, Math.min(rawTop, viewport.height - gap - panelHeight)),\n left: Math.max(gap, Math.min(rawLeft, viewport.width - width - gap)),\n width,\n side,\n };\n}\n\n/**\n * Show `panel` beside `anchor` once. Uses the browser's top layer when it can\n * (so dialogs and scrolling containers never clip the panel), otherwise\n * position: fixed. Returns a function that hides the panel again.\n */\nexport function placePopover(\n anchor: Element,\n panel: HTMLElement,\n options: PlaceOptions = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const topLayer = supportsPopover();\n if (topLayer) {\n if (!panel.hasAttribute('popover')) {\n panel.setAttribute('popover', 'manual');\n }\n if (!panel.matches(':popover-open')) {\n panel.showPopover();\n }\n }\n const layout = resolveLayout(options.layout, window.innerWidth, options.sheetBelow);\n panel.dataset['layout'] = layout;\n if (layout === 'sheet') {\n // The stylesheet pins a sheet to the bottom edge; clear any popover coordinates.\n Object.assign(panel.style, {\n top: '',\n left: '',\n width: '',\n position: 'fixed',\n margin: '0',\n inset: 'auto 0 0 0',\n });\n delete panel.dataset['side'];\n\n return hider(panel, topLayer);\n }\n const position = computePosition(\n anchor.getBoundingClientRect(),\n panel.offsetHeight,\n { width: window.innerWidth, height: window.innerHeight },\n options,\n );\n Object.assign(panel.style, {\n position: 'fixed',\n margin: '0',\n inset: 'auto',\n top: `${position.top}px`,\n left: `${position.left}px`,\n width: `${position.width}px`,\n });\n panel.dataset['side'] = position.side;\n\n return hider(panel, topLayer);\n}\n\nfunction hider(panel: HTMLElement, topLayer: boolean): () => void {\n return () => {\n if (topLayer && panel.matches(':popover-open')) {\n panel.hidePopover();\n }\n };\n}\n\n/**\n * Show the panel and keep it beside the anchor while the window resizes or\n * any ancestor scrolls. Returns a function that stops tracking and hides it.\n */\nexport function trackPosition(\n anchor: Element,\n panel: HTMLElement,\n options: PlaceOptions = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n let hide = placePopover(anchor, panel, options);\n const place = (): void => {\n hide = placePopover(anchor, panel, options);\n };\n window.addEventListener('resize', place);\n window.addEventListener('scroll', place, true);\n\n return () => {\n window.removeEventListener('resize', place);\n window.removeEventListener('scroll', place, true);\n hide();\n };\n}\n\nfunction noop(): void {\n // Nothing to undo outside a browser.\n}\n","import { hasDom } from './env';\n\ntype Targets =\n | readonly (Element | null | undefined)[]\n | (() => readonly (Element | null | undefined)[]);\n\n/**\n * Call `callback` when a pointer goes down outside all of `elements` (checked\n * in the capture phase, before the page reacts). Returns a cleanup function.\n */\nexport function onOutsidePointer(\n elements: Targets,\n callback: (event: PointerEvent) => void,\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const listener = (event: PointerEvent): void => {\n const target = event.target;\n const list = typeof elements === 'function' ? elements() : elements;\n const inside =\n target instanceof Node &&\n list.some((element) => element?.contains(target) ?? false);\n if (!inside) {\n callback(event);\n }\n };\n document.addEventListener('pointerdown', listener, true);\n\n return () => {\n document.removeEventListener('pointerdown', listener, true);\n };\n}\n\n/**\n * Call `callback` when the pointer goes down on a panel's backdrop (the dimmed\n * area around a bottom sheet). A top-layer panel receives those events itself,\n * so they are told apart by position: on the panel, but outside its box.\n */\nexport function onBackdropPointer(\n panel: HTMLElement,\n callback: (event: PointerEvent) => void,\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const listener = (event: PointerEvent): void => {\n if (event.target !== panel) {\n return;\n }\n const box = panel.getBoundingClientRect();\n const outside =\n event.clientX < box.left ||\n event.clientX > box.right ||\n event.clientY < box.top ||\n event.clientY > box.bottom;\n if (outside) {\n callback(event);\n }\n };\n panel.addEventListener('pointerdown', listener);\n\n return () => {\n panel.removeEventListener('pointerdown', listener);\n };\n}\n\n/**\n * Call `callback` on Escape. By default it listens in the capture phase and\n * stops the event, so an enclosing dialog does not close as well.\n */\nexport function onEscape(\n callback: (event: KeyboardEvent) => void,\n options: { readonly capture?: boolean; readonly stop?: boolean } = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const capture = options.capture ?? true;\n const listener = (event: KeyboardEvent): void => {\n if (event.key !== 'Escape') {\n return;\n }\n if (options.stop ?? true) {\n event.stopPropagation();\n event.preventDefault();\n }\n callback(event);\n };\n window.addEventListener('keydown', listener, capture);\n\n return () => {\n window.removeEventListener('keydown', listener, capture);\n };\n}\n\n/** Focus the first element in `container` matching `selector` (the roving-tabindex day by default). */\nexport function focusFirst(\n container: ParentNode | null,\n selector = '[tabindex=\"0\"]',\n): boolean {\n const element = container?.querySelector<HTMLElement>(selector);\n if (!element) {\n return false;\n }\n element.focus();\n\n return true;\n}\n\n/** Put focus back, e.g. on the input after the panel closes. */\nexport function restoreFocus(element: HTMLElement | null | undefined): void {\n element?.focus({ preventScroll: true });\n}\n\nfunction noop(): void {\n // Nothing to undo outside a browser.\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["noop"],"sources":["../src/env.ts","../src/position.ts","../src/events.ts"],"sourcesContent":["/** True in a browser-like environment; false during server rendering. */\nexport function hasDom(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/** Whether the browser can show elements in the top layer (`popover` + `showPopover`). */\nexport function supportsPopover(): boolean {\n return (\n hasDom() &&\n typeof HTMLElement !== 'undefined' &&\n typeof HTMLElement.prototype.showPopover === 'function'\n );\n}\n\n/** Whether the user asked the system for less motion. */\nexport function prefersReducedMotion(): boolean {\n return (\n hasDom() &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n}\n","import { hasDom, supportsPopover } from './env';\n\nexport type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';\n\n/** 'popover' sits beside the anchor; 'sheet' is a bottom sheet; 'auto' picks the sheet on narrow screens. */\nexport type PanelLayout = 'auto' | 'popover' | 'sheet';\n\nexport interface PlaceOptions {\n /** Space between the anchor and the panel, and kept from the viewport edges. Default 6. */\n readonly gap?: number | undefined;\n /** Panel width in px; shrinks to fit narrow screens. Default 320. */\n readonly width?: number | undefined;\n /** Preferred side; flips when there is not enough room. Default 'bottom-start'. */\n readonly placement?: Placement | undefined;\n /** Default 'auto'. */\n readonly layout?: PanelLayout | undefined;\n /** 'auto' uses the sheet below this viewport width, in px. Default 480. */\n readonly sheetBelow?: number | undefined;\n}\n\n/** The layout to use for a viewport width. Pure. */\nexport function resolveLayout(\n layout: PanelLayout | undefined,\n viewportWidth: number,\n sheetBelow = 480,\n): 'popover' | 'sheet' {\n if (layout !== undefined && layout !== 'auto') {\n return layout;\n }\n\n return viewportWidth < sheetBelow ? 'sheet' : 'popover';\n}\n\nexport interface Rect {\n readonly top: number;\n readonly left: number;\n readonly bottom: number;\n readonly right: number;\n}\n\nexport interface Position {\n readonly top: number;\n readonly left: number;\n readonly width: number;\n /** Where the panel ended up after flipping. */\n readonly side: 'top' | 'bottom';\n}\n\n/**\n * Where to put a panel next to an anchor, in viewport (position: fixed)\n * coordinates. Pure, so it can be tested without a browser. Rules from the\n * institute-SaaS picker: prefer the requested side, flip when the panel does\n * not fit but does fit on the other side, and keep `gap` px from every edge\n * (the panel may overlap the anchor rather than leave the screen).\n */\nexport function computePosition(\n anchor: Rect,\n panelHeight: number,\n viewport: { readonly width: number; readonly height: number },\n options: PlaceOptions = {},\n): Position {\n const gap = options.gap ?? 6;\n const placement = options.placement ?? 'bottom-start';\n const width = Math.max(0, Math.min(options.width ?? 320, viewport.width - gap * 2));\n const roomBelow = viewport.height - anchor.bottom - gap * 2;\n const roomAbove = anchor.top - gap * 2;\n const wantsTop = placement.startsWith('top');\n const side: 'top' | 'bottom' = wantsTop\n ? roomAbove >= panelHeight || roomAbove >= roomBelow\n ? 'top'\n : 'bottom'\n : roomBelow >= panelHeight || roomBelow >= roomAbove\n ? 'bottom'\n : 'top';\n const rawTop = side === 'bottom' ? anchor.bottom + gap : anchor.top - gap - panelHeight;\n const rawLeft = placement.endsWith('end') ? anchor.right - width : anchor.left;\n\n return {\n // Keep the whole panel on screen; when it is taller than the room, its top edge wins.\n top: Math.max(gap, Math.min(rawTop, viewport.height - gap - panelHeight)),\n left: Math.max(gap, Math.min(rawLeft, viewport.width - width - gap)),\n width,\n side,\n };\n}\n\n/**\n * Show `panel` beside `anchor` once. Uses the browser's top layer when it can\n * (so dialogs and scrolling containers never clip the panel), otherwise\n * position: fixed. Returns a function that hides the panel again.\n */\nexport function placePopover(\n anchor: Element,\n panel: HTMLElement,\n options: PlaceOptions = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const topLayer = supportsPopover();\n if (topLayer) {\n if (!panel.hasAttribute('popover')) {\n panel.setAttribute('popover', 'manual');\n }\n if (!panel.matches(':popover-open')) {\n panel.showPopover();\n }\n }\n const layout = resolveLayout(options.layout, window.innerWidth, options.sheetBelow);\n panel.dataset['layout'] = layout;\n if (layout === 'sheet') {\n // The stylesheet pins a sheet to the bottom edge; clear any popover coordinates.\n Object.assign(panel.style, {\n top: '',\n left: '',\n width: '',\n position: 'fixed',\n margin: '0',\n inset: 'auto 0 0 0',\n });\n delete panel.dataset['side'];\n\n return hider(panel, topLayer);\n }\n const position = computePosition(\n anchor.getBoundingClientRect(),\n panel.offsetHeight,\n { width: window.innerWidth, height: window.innerHeight },\n options,\n );\n Object.assign(panel.style, {\n position: 'fixed',\n margin: '0',\n inset: 'auto',\n top: `${position.top}px`,\n left: `${position.left}px`,\n width: `${position.width}px`,\n });\n panel.dataset['side'] = position.side;\n\n return hider(panel, topLayer);\n}\n\nfunction hider(panel: HTMLElement, topLayer: boolean): () => void {\n return () => {\n if (topLayer && panel.matches(':popover-open')) {\n panel.hidePopover();\n }\n };\n}\n\n/**\n * Show the panel and keep it beside the anchor while the window resizes or\n * any ancestor scrolls. Returns a function that stops tracking and hides it.\n */\nexport function trackPosition(\n anchor: Element,\n panel: HTMLElement,\n options: PlaceOptions = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n let hide = placePopover(anchor, panel, options);\n let frame: number | null = null;\n const place = (): void => {\n hide = placePopover(anchor, panel, options);\n };\n // Scroll events can outnumber frames, and each placement measures the page.\n const placeOnNextFrame = (): void => {\n frame ??= requestAnimationFrame(() => {\n frame = null;\n place();\n });\n };\n window.addEventListener('resize', place);\n window.addEventListener('scroll', placeOnNextFrame, true);\n\n return () => {\n window.removeEventListener('resize', place);\n window.removeEventListener('scroll', placeOnNextFrame, true);\n if (frame !== null) {\n cancelAnimationFrame(frame);\n }\n hide();\n };\n}\n\nfunction noop(): void {\n // Nothing to undo outside a browser.\n}\n","import { hasDom } from './env';\n\ntype Targets =\n | readonly (Element | null | undefined)[]\n | (() => readonly (Element | null | undefined)[]);\n\n/**\n * Call `callback` when a pointer goes down outside all of `elements` (checked\n * in the capture phase, before the page reacts). Returns a cleanup function.\n */\nexport function onOutsidePointer(\n elements: Targets,\n callback: (event: PointerEvent) => void,\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const listener = (event: PointerEvent): void => {\n const target = event.target;\n const list = typeof elements === 'function' ? elements() : elements;\n const inside =\n target instanceof Node &&\n list.some((element) => element?.contains(target) ?? false);\n if (!inside) {\n callback(event);\n }\n };\n document.addEventListener('pointerdown', listener, true);\n\n return () => {\n document.removeEventListener('pointerdown', listener, true);\n };\n}\n\n/**\n * Call `callback` when the pointer goes down on a panel's backdrop (the dimmed\n * area around a bottom sheet). A top-layer panel receives those events itself,\n * so they are told apart by position: on the panel, but outside its box.\n */\nexport function onBackdropPointer(\n panel: HTMLElement,\n callback: (event: PointerEvent) => void,\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const listener = (event: PointerEvent): void => {\n if (event.target !== panel) {\n return;\n }\n const box = panel.getBoundingClientRect();\n const outside =\n event.clientX < box.left ||\n event.clientX > box.right ||\n event.clientY < box.top ||\n event.clientY > box.bottom;\n if (outside) {\n callback(event);\n }\n };\n panel.addEventListener('pointerdown', listener);\n\n return () => {\n panel.removeEventListener('pointerdown', listener);\n };\n}\n\n/**\n * Call `callback` on Escape. By default it listens in the capture phase and\n * stops the event, so an enclosing dialog does not close as well.\n */\nexport function onEscape(\n callback: (event: KeyboardEvent) => void,\n options: { readonly capture?: boolean; readonly stop?: boolean } = {},\n): () => void {\n if (!hasDom()) {\n return noop;\n }\n const capture = options.capture ?? true;\n const listener = (event: KeyboardEvent): void => {\n if (event.key !== 'Escape') {\n return;\n }\n if (options.stop ?? true) {\n event.stopPropagation();\n event.preventDefault();\n }\n callback(event);\n };\n window.addEventListener('keydown', listener, capture);\n\n return () => {\n window.removeEventListener('keydown', listener, capture);\n };\n}\n\n/** Focus the first element in `container` matching `selector` (the roving-tabindex day by default). */\nexport function focusFirst(\n container: ParentNode | null,\n selector = '[tabindex=\"0\"]',\n): boolean {\n const element = container?.querySelector<HTMLElement>(selector);\n if (!element) {\n return false;\n }\n element.focus();\n\n return true;\n}\n\n/** Put focus back, e.g. on the input after the panel closes. */\nexport function restoreFocus(element: HTMLElement | null | undefined): void {\n element?.focus({ preventScroll: true });\n}\n\nfunction noop(): void {\n // Nothing to undo outside a browser.\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/position.ts
CHANGED
|
@@ -162,15 +162,26 @@ export function trackPosition(
|
|
|
162
162
|
return noop;
|
|
163
163
|
}
|
|
164
164
|
let hide = placePopover(anchor, panel, options);
|
|
165
|
+
let frame: number | null = null;
|
|
165
166
|
const place = (): void => {
|
|
166
167
|
hide = placePopover(anchor, panel, options);
|
|
167
168
|
};
|
|
169
|
+
// Scroll events can outnumber frames, and each placement measures the page.
|
|
170
|
+
const placeOnNextFrame = (): void => {
|
|
171
|
+
frame ??= requestAnimationFrame(() => {
|
|
172
|
+
frame = null;
|
|
173
|
+
place();
|
|
174
|
+
});
|
|
175
|
+
};
|
|
168
176
|
window.addEventListener('resize', place);
|
|
169
|
-
window.addEventListener('scroll',
|
|
177
|
+
window.addEventListener('scroll', placeOnNextFrame, true);
|
|
170
178
|
|
|
171
179
|
return () => {
|
|
172
180
|
window.removeEventListener('resize', place);
|
|
173
|
-
window.removeEventListener('scroll',
|
|
181
|
+
window.removeEventListener('scroll', placeOnNextFrame, true);
|
|
182
|
+
if (frame !== null) {
|
|
183
|
+
cancelAnimationFrame(frame);
|
|
184
|
+
}
|
|
174
185
|
hide();
|
|
175
186
|
};
|
|
176
187
|
}
|