@neovici/cosmoz-dropdown 3.0.2 → 3.2.0

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.
@@ -0,0 +1,13 @@
1
+ import { TemplateResult } from 'lit-html';
2
+ import { Placement } from './use-position';
3
+ import { UseFocusOpts } from './use-focus';
4
+ interface ContentProps {
5
+ anchor: HTMLElement;
6
+ placement?: Placement;
7
+ render?: () => TemplateResult;
8
+ }
9
+ declare const Content: (host: HTMLElement & ContentProps) => TemplateResult<1>;
10
+ interface DropdownProps extends UseFocusOpts, Pick<ContentProps, 'placement' | 'render'> {
11
+ }
12
+ declare const Dropdown: (host: HTMLElement & DropdownProps) => TemplateResult<1>;
13
+ export { Dropdown, Content };
@@ -0,0 +1,150 @@
1
+ import { component, useCallback } from 'haunted';
2
+ import { html, nothing } from 'lit-html';
3
+ import { when } from 'lit-html/directives/when.js';
4
+ import { usePosition } from './use-position';
5
+ import { useHostFocus } from './use-focus';
6
+ const preventDefault = (e) => e.preventDefault();
7
+ const Content = (host) => {
8
+ const { anchor, placement, render } = host;
9
+ usePosition({ anchor, placement, host });
10
+ return html ` <style>
11
+ :host {
12
+ position: fixed;
13
+ left: -9999999999px;
14
+ min-width: 72px;
15
+ box-sizing: border-box;
16
+ padding: var(--cosmoz-dropdown-spacing, 0px);
17
+ z-index: var(--cosmoz-dropdown-z-index, 2);
18
+ }
19
+ .wrap {
20
+ background: var(--cosmoz-dropdown-bg-color, #fff);
21
+ box-shadow: var(
22
+ --cosmoz-dropdown-box-shadow,
23
+ 0px 3px 4px 2px rgba(0, 0, 0, 0.1)
24
+ );
25
+ }
26
+ ::slotted(*) {
27
+ display: block;
28
+ }
29
+ </style>
30
+ <div class="wrap" part="wrap"><slot></slot>${render?.() || nothing}</div>`;
31
+ };
32
+ const Dropdown = (host) => {
33
+ const { placement, render } = host, anchor = useCallback(() => host.shadowRoot.querySelector('.anchor'), []), { active, onToggle } = useHostFocus(host);
34
+ return html ` <style>
35
+ .anchor {
36
+ pointer-events: none;
37
+ padding: var(--cosmoz-dropdown-anchor-spacing);
38
+ }
39
+ button {
40
+ border: none;
41
+ cursor: pointer;
42
+ position: relative;
43
+ pointer-events: auto;
44
+ outline: none;
45
+ background: var(
46
+ --cosmoz-dropdown-button-bg-color,
47
+ var(--cosmoz-button-bg-color, #101010)
48
+ );
49
+ color: var(
50
+ --cosmoz-dropdown-button-color,
51
+ var(--cosmoz-button-color, #fff)
52
+ );
53
+ border-radius: var(--cosmoz-dropdown-button-radius, 50%);
54
+ width: var(
55
+ --cosmoz-dropdown-button-width,
56
+ var(--cosmoz-dropdown-button-size, 40px)
57
+ );
58
+ height: var(
59
+ --cosmoz-dropdown-button-height,
60
+ var(--cosmoz-dropdown-button-size, 40px)
61
+ );
62
+ padding: var(--cosmoz-dropdown-button-padding);
63
+ }
64
+ button:hover {
65
+ background: var(
66
+ --cosmoz-dropdown-button-hover-bg-color,
67
+ var(--cosmoz-button-hover-bg-color, #3a3f44)
68
+ );
69
+ }
70
+ ::slotted(svg) {
71
+ pointer-events: none;
72
+ }
73
+ @-moz-document url-prefix() {
74
+ #content {
75
+ left: auto;
76
+ }
77
+ }
78
+ </style>
79
+ <div class="anchor" part="anchor">
80
+ <button
81
+ @click=${onToggle}
82
+ @mousedown=${preventDefault}
83
+ part="button"
84
+ id="dropdownButton"
85
+ >
86
+ <slot name="button">...</slot>
87
+ </button>
88
+ </div>
89
+ ${when(active, () => html ` <cosmoz-dropdown-content
90
+ id="content"
91
+ part="content"
92
+ exportparts="wrap, content"
93
+ .anchor=${anchor}
94
+ .placement=${placement}
95
+ .render=${render}
96
+ >
97
+ <slot></slot>
98
+ </cosmoz-dropdown-content>`)}`;
99
+ };
100
+ const List = () => html `
101
+ <style>
102
+ :host {
103
+ display: contents;
104
+ max-height: var(--cosmoz-dropdown-menu-max-height, calc(96vh - 64px));
105
+ overflow-y: auto;
106
+ }
107
+ ::slotted(:not(slot)) {
108
+ display: block;
109
+ --paper-button_-_display: block;
110
+ box-sizing: border-box;
111
+ padding: 10px 24px;
112
+ background: transparent;
113
+ color: var(--cosmoz-dropdown-menu-color, #101010);
114
+ transition: background 0.25s, color 0.25s;
115
+ border: none;
116
+ cursor: pointer;
117
+ font-size: 14px;
118
+ line-height: 20px;
119
+ text-align: left;
120
+ margin: 0;
121
+ width: 100%;
122
+ }
123
+
124
+ ::slotted(:not(slot):hover) {
125
+ background: var(
126
+ --cosmoz-dropdown-menu-hover-color,
127
+ var(--cosmoz-selection-color, rgba(58, 145, 226, 0.1))
128
+ );
129
+ }
130
+
131
+ ::slotted(:not(slot)[disabled]) {
132
+ opacity: 0.5;
133
+ pointer-events: none;
134
+ }
135
+ </style>
136
+ <slot></slot>
137
+ `;
138
+ const Menu = ({ placement }) => html ` <cosmoz-dropdown
139
+ .placement=${placement}
140
+ part="dropdown"
141
+ exportparts="anchor, button, content, wrap, dropdown"
142
+ >
143
+ <slot name="button" slot="button"></slot>
144
+ <cosmoz-dropdown-list><slot></slot></cosmoz-dropdown-list>
145
+ </cosmoz-dropdown>`;
146
+ customElements.define('cosmoz-dropdown-content', component(Content));
147
+ customElements.define('cosmoz-dropdown', component(Dropdown));
148
+ customElements.define('cosmoz-dropdown-list', component(List));
149
+ customElements.define('cosmoz-dropdown-menu', component(Menu));
150
+ export { Dropdown, Content };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './cosmoz-dropdown';
2
+ export * from './use-focus';
3
+ export * from './use-position';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Listen to a scroll event in a ancestor root
3
+ * @param {HTMLElement} el The element
4
+ * @param {function} handler The event handler
5
+ * @returns {function} Function to cleanup the event listeners
6
+ */
7
+ export declare const onScrolled: <T extends Element>(el: T, handler: (e: Event) => void) => () => void;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Returns all ancestor ShadowRoot elements
3
+ * @param {HTMLElement} el The element
4
+ * @param {HTMLElement} limit The limit to stop searching
5
+ * @returns {ShadowRoot[]} Array of ancestor roots
6
+ */
7
+ const ancestorRoots = (el, limit = document.body) => {
8
+ const roots = [];
9
+ let ancestor = el;
10
+ while (ancestor && ancestor !== limit) {
11
+ if (ancestor instanceof Element && ancestor.assignedSlot) {
12
+ ancestor = ancestor.assignedSlot;
13
+ continue;
14
+ }
15
+ if (ancestor instanceof ShadowRoot) {
16
+ roots.push(ancestor);
17
+ ancestor = ancestor.host;
18
+ continue;
19
+ }
20
+ ancestor = ancestor.parentNode;
21
+ }
22
+ return roots;
23
+ };
24
+ /**
25
+ * Listen to a scroll event in a ancestor root
26
+ * @param {HTMLElement} el The element
27
+ * @param {function} handler The event handler
28
+ * @returns {function} Function to cleanup the event listeners
29
+ */
30
+ export const onScrolled = (el, handler) => {
31
+ const roots = ancestorRoots(el);
32
+ roots.forEach((r) => r.addEventListener('scroll', handler, true));
33
+ return () => {
34
+ roots.forEach((r) => r.removeEventListener('scroll', handler, true));
35
+ };
36
+ };
@@ -0,0 +1,16 @@
1
+ export interface UseFocusOpts {
2
+ disabled?: boolean;
3
+ onFocus: (focused: boolean) => void;
4
+ }
5
+ export declare const useFocus: ({ disabled, onFocus }: UseFocusOpts) => {
6
+ active: boolean | undefined;
7
+ setClosed: (closed: boolean) => void;
8
+ onToggle: (e: Event) => void;
9
+ onFocus: (e: FocusEvent) => void;
10
+ };
11
+ export declare const useHostFocus: (host: HTMLElement & UseFocusOpts) => {
12
+ active: boolean | undefined;
13
+ setClosed: (closed: boolean) => void;
14
+ onToggle: (e: Event) => void;
15
+ onFocus: (e: FocusEvent) => void;
16
+ };
@@ -0,0 +1,54 @@
1
+ import { useEffect, useState, useCallback } from 'haunted';
2
+ import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
3
+ const isFocused = (t) => t.matches(':focus-within');
4
+ export const useFocus = ({ disabled, onFocus }) => {
5
+ const [focusState, setState] = useState(), { focused, closed } = focusState || {}, active = focused && !disabled, meta = useMeta({ closed, onFocus }), setClosed = useCallback((closed) => setState((p) => ({ ...p, closed })), []), onToggle = useCallback((e) => {
6
+ const target = e.currentTarget;
7
+ return isFocused(target)
8
+ ? setState((p) => ({ focused: true, closed: !p?.closed }))
9
+ : target.focus();
10
+ }, []);
11
+ useEffect(() => {
12
+ if (!active) {
13
+ return;
14
+ }
15
+ const handler = (e) => {
16
+ if (e.defaultPrevented) {
17
+ return;
18
+ }
19
+ const { closed } = meta;
20
+ if (e.key === 'Escape' && !closed) {
21
+ e.preventDefault();
22
+ setClosed(true);
23
+ }
24
+ else if (['ArrowUp', 'Up'].includes(e.key) && closed) {
25
+ e.preventDefault();
26
+ setClosed(false);
27
+ }
28
+ };
29
+ document.addEventListener('keydown', handler, true);
30
+ return () => document.removeEventListener('keydown', handler, true);
31
+ }, [active]);
32
+ return {
33
+ active: active && !closed,
34
+ setClosed,
35
+ onToggle,
36
+ onFocus: useCallback((e) => {
37
+ const focused = isFocused(e.currentTarget);
38
+ setState({ focused });
39
+ meta.onFocus?.(focused);
40
+ }, [meta]),
41
+ };
42
+ };
43
+ const fevs = ['focusin', 'focusout'];
44
+ export const useHostFocus = (host) => {
45
+ const thru = useFocus(host), { onFocus } = thru;
46
+ useEffect(() => {
47
+ host.setAttribute('tabindex', '-1');
48
+ fevs.forEach((ev) => host.addEventListener(ev, onFocus));
49
+ return () => {
50
+ fevs.forEach((ev) => host.removeEventListener(ev, onFocus));
51
+ };
52
+ }, []);
53
+ return thru;
54
+ };
@@ -0,0 +1,16 @@
1
+ /// <reference path="../types/position.d.ts" />
2
+ import type { Placement } from 'position.js';
3
+ export declare const defaultPlacement: readonly ["bottom-left", "bottom-right", "bottom", "top-left", "top-right", "top"];
4
+ export { Placement };
5
+ export interface PositionOpts {
6
+ host: HTMLElement;
7
+ anchor: HTMLElement;
8
+ placement?: Placement;
9
+ confinement?: HTMLElement;
10
+ limit?: boolean;
11
+ }
12
+ export declare const position: ({ host, anchor, placement, confinement, limit, }: PositionOpts) => void;
13
+ interface UsePositionOpts extends Omit<PositionOpts, 'anchor'> {
14
+ anchor?: (() => HTMLElement) | HTMLElement | null;
15
+ }
16
+ export declare const usePosition: ({ anchor: anchorage, host, ...thru }: UsePositionOpts) => void;
@@ -0,0 +1,49 @@
1
+ // eslint-disable-next-line
2
+ /// <reference path="../types/position.d.ts" />
3
+ import { useEffect } from 'haunted';
4
+ import getPosition from 'position.js';
5
+ import { onScrolled } from './on-scrolled';
6
+ export const defaultPlacement = [
7
+ 'bottom-left',
8
+ 'bottom-right',
9
+ 'bottom',
10
+ 'top-left',
11
+ 'top-right',
12
+ 'top',
13
+ ];
14
+ export const position = ({ host, anchor, placement = defaultPlacement, confinement, limit, }) => {
15
+ const anchorBounds = anchor.getBoundingClientRect(), hostBounds = host.getBoundingClientRect(), { popupOffset: offset } = getPosition(hostBounds, anchorBounds, placement, {
16
+ fixed: true,
17
+ adjustXY: 'both',
18
+ offsetParent: confinement,
19
+ }), { style } = host;
20
+ style.left = offset.left + 'px';
21
+ style.top = offset.top + 'px';
22
+ if (limit) {
23
+ style.minWidth = Math.max(anchorBounds.width, hostBounds.width) + 'px';
24
+ }
25
+ };
26
+ export const usePosition = ({ anchor: anchorage, host, ...thru }) => {
27
+ useEffect(() => {
28
+ const anchor = typeof anchorage === 'function' ? anchorage() : anchorage;
29
+ if (anchor == null) {
30
+ return;
31
+ }
32
+ let rid;
33
+ const reposition = () => position({ host, anchor, ...thru }), ro = new ResizeObserver(reposition);
34
+ ro.observe(host);
35
+ ro.observe(anchor);
36
+ const onReposition = () => {
37
+ cancelAnimationFrame(rid);
38
+ rid = requestAnimationFrame(reposition);
39
+ }, offScroll = onScrolled(anchor, onReposition);
40
+ window.addEventListener('resize', onReposition, true);
41
+ return () => {
42
+ ro.unobserve(host);
43
+ ro.unobserve(anchor);
44
+ offScroll();
45
+ window.removeEventListener('resize', onReposition, true);
46
+ cancelAnimationFrame(rid);
47
+ };
48
+ }, [anchorage, ...Object.values(thru)]);
49
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-dropdown",
3
- "version": "3.0.2",
3
+ "version": "3.2.0",
4
4
  "description": "A simple dropdown web component",
5
5
  "keywords": [
6
6
  "lit-html",
@@ -16,13 +16,13 @@
16
16
  },
17
17
  "license": "Apache-2.0",
18
18
  "author": "",
19
- "main": "src/index.js",
19
+ "main": "dist/index.js",
20
20
  "directories": {
21
21
  "test": "test"
22
22
  },
23
23
  "scripts": {
24
- "lint": "eslint --cache --ext .js .",
25
- "lint-tsc": "tsc",
24
+ "lint": "tsc && eslint --cache .",
25
+ "build": "tsc -p tsconfig.build.json",
26
26
  "start": "wds",
27
27
  "test": "wtr --coverage",
28
28
  "test:watch": "wtr --watch",
@@ -46,7 +46,8 @@
46
46
  "access": "public"
47
47
  },
48
48
  "files": [
49
- "src/*.js"
49
+ "dist/",
50
+ "types"
50
51
  ],
51
52
  "commitlint": {
52
53
  "extends": [
@@ -60,6 +61,13 @@
60
61
  ]
61
62
  }
62
63
  },
64
+ "exports": {
65
+ ".": "./dist/index.js",
66
+ "./use-focus": "./dist/use-focus.js",
67
+ "./use-position": "./dist/use-position.js",
68
+ "./src/use-focus.js": "./dist/use-focus.js",
69
+ "./src/use-position.js": "./dist/use-position.js"
70
+ },
63
71
  "dependencies": {
64
72
  "@neovici/cosmoz-utils": "^5.0.0",
65
73
  "haunted": "^5.0.0",
@@ -74,13 +82,10 @@
74
82
  "@semantic-release/changelog": "^6.0.0",
75
83
  "@semantic-release/git": "^10.0.0",
76
84
  "@storybook/storybook-deployer": "^2.8.5",
77
- "@web/dev-server": "^0.1.28",
78
- "@web/dev-server-storybook": "^0.5.1",
79
- "@web/test-runner": "^0.13.0",
85
+ "@web/dev-server-storybook": "^0.6.0",
80
86
  "husky": "^8.0.0",
81
- "prettier": "^2.5.1",
82
- "semantic-release": "^19.0.0",
83
- "sinon": "^14.0.0",
84
- "typescript": "^4.0.0"
87
+ "semantic-release": "^21.0.0",
88
+ "sinon": "^15.0.0",
89
+ "typescript": "^5.0.0"
85
90
  }
86
91
  }
@@ -0,0 +1,49 @@
1
+ declare module 'position.js' {
2
+ export type PlacementDirection =
3
+ | 'bottom'
4
+ | 'top'
5
+ | 'right'
6
+ | 'left'
7
+ | 'center'
8
+ | 'left-center'
9
+ | 'top-center'
10
+ | 'bottom-center'
11
+ | 'bottom-left'
12
+ | 'bottom-right'
13
+ | 'bottom'
14
+ | 'top-left'
15
+ | 'top-right'
16
+ | 'top';
17
+
18
+ export interface PlacementPrecise {
19
+ popup: PlacementDirection;
20
+ anchor: PlacementDirection;
21
+ }
22
+
23
+ export type Placement =
24
+ | PlacementPrecise
25
+ | PlacementDirection
26
+ | readonly PlacementDirection[]
27
+ | readonly PlacementPrecise[];
28
+
29
+ export interface Bounds {
30
+ top: number;
31
+ left: number;
32
+ }
33
+
34
+ export default function position(
35
+ hostBounds: Bounds,
36
+ anchorBounds: Bounds,
37
+ placement: Placement,
38
+ options?: {
39
+ fixed?: boolean;
40
+ adjustXY?: 'both';
41
+ offsetParent?: HTMLElement;
42
+ }
43
+ ): {
44
+ popupOffset: {
45
+ top: number;
46
+ left: number;
47
+ };
48
+ };
49
+ }
@@ -1,163 +0,0 @@
1
- import { component, useCallback } from 'haunted';
2
- import { html, nothing } from 'lit-html';
3
- import { when } from 'lit-html/directives/when.js';
4
- import { usePosition } from './use-position';
5
- import { useHostFocus } from './use-focus';
6
-
7
- const preventDefault = (e) => e.preventDefault(),
8
- Content = (host) => {
9
- const { anchor, placement, render } = host;
10
- usePosition({ anchor, placement, host });
11
- return html` <style>
12
- :host {
13
- position: fixed;
14
- left: -9999999999px;
15
- min-width: 72px;
16
- box-sizing: border-box;
17
- padding: var(--cosmoz-dropdown-spacing, 0px);
18
- z-index: var(--cosmoz-dropdown-z-index, 2);
19
- }
20
- .wrap {
21
- background: var(--cosmoz-dropdown-bg-color, #fff);
22
- box-shadow: var(
23
- --cosmoz-dropdown-box-shadow,
24
- 0px 3px 4px 2px rgba(0, 0, 0, 0.1)
25
- );
26
- }
27
- ::slotted(*) {
28
- display: block;
29
- }
30
- </style>
31
- <div class="wrap" part="wrap">
32
- <slot></slot>${render?.() || nothing}
33
- </div>`;
34
- },
35
- Dropdown = (host) => {
36
- const { placement, render } = host,
37
- anchor = useCallback(() => host.shadowRoot.querySelector('.anchor'), []),
38
- { active, onToggle } = useHostFocus(host);
39
- return html`
40
- <style>
41
- .anchor {
42
- pointer-events: none;
43
- padding: var(--cosmoz-dropdown-anchor-spacing);
44
- }
45
- button {
46
- border: none;
47
- cursor: pointer;
48
- position: relative;
49
- pointer-events: auto;
50
- outline: none;
51
- background: var(
52
- --cosmoz-dropdown-button-bg-color,
53
- var(--cosmoz-button-bg-color, #101010)
54
- );
55
- color: var(
56
- --cosmoz-dropdown-button-color,
57
- var(--cosmoz-button-color, #fff)
58
- );
59
- border-radius: var(--cosmoz-dropdown-button-radius, 50%);
60
- width: var(
61
- --cosmoz-dropdown-button-width,
62
- var(--cosmoz-dropdown-button-size, 40px)
63
- );
64
- height: var(
65
- --cosmoz-dropdown-button-height,
66
- var(--cosmoz-dropdown-button-size, 40px)
67
- );
68
- padding: var(--cosmoz-dropdown-button-padding);
69
- }
70
- button:hover {
71
- background: var(
72
- --cosmoz-dropdown-button-hover-bg-color,
73
- var(--cosmoz-button-hover-bg-color, #3a3f44)
74
- );
75
- }
76
- ::slotted(svg) {
77
- pointer-events: none;
78
- }
79
- @-moz-document url-prefix() {
80
- #content {
81
- left: auto;
82
- }
83
- }
84
- </style>
85
- <div class="anchor" part="anchor">
86
- <button
87
- @click=${onToggle}
88
- @mousedown=${preventDefault}
89
- part="button"
90
- id="dropdownButton"
91
- >
92
- <slot name="button">...</slot>
93
- </button>
94
- </div>
95
- ${when(
96
- active,
97
- () =>
98
- html` <cosmoz-dropdown-content
99
- id="content"
100
- part="content"
101
- exportparts="wrap, content"
102
- .anchor=${anchor}
103
- .placement=${placement}
104
- .render=${render}
105
- >
106
- <slot></slot>
107
- </cosmoz-dropdown-content>`
108
- )}
109
- `;
110
- },
111
- List = () => html`
112
- <style>
113
- :host {
114
- display: contents;
115
- max-height: var(--cosmoz-dropdown-menu-max-height, calc(96vh - 64px));
116
- overflow-y: auto;
117
- }
118
- ::slotted(:not(slot)) {
119
- display: block;
120
- --paper-button_-_display: block;
121
- box-sizing: border-box;
122
- padding: 10px 24px;
123
- background: transparent;
124
- color: var(--cosmoz-dropdown-menu-color, #101010);
125
- transition: background 0.25s, color 0.25s;
126
- border: none;
127
- cursor: pointer;
128
- font-size: 14px;
129
- line-height: 20px;
130
- text-align: left;
131
- margin: 0;
132
- width: 100%;
133
- }
134
-
135
- ::slotted(:not(slot):hover) {
136
- background: var(
137
- --cosmoz-dropdown-menu-hover-color,
138
- var(--cosmoz-selection-color, rgba(58, 145, 226, 0.1))
139
- );
140
- }
141
-
142
- ::slotted(:not(slot)[disabled]) {
143
- opacity: 0.5;
144
- pointer-events: none;
145
- }
146
- </style>
147
- <slot></slot>
148
- `,
149
- Menu = ({ placement }) => html` <cosmoz-dropdown
150
- .placement=${placement}
151
- part="dropdown"
152
- exportparts="anchor, button, content, wrap, dropdown"
153
- >
154
- <slot name="button" slot="button"></slot>
155
- <cosmoz-dropdown-list><slot></slot></cosmoz-dropdown-list>
156
- </cosmoz-dropdown>`;
157
-
158
- customElements.define('cosmoz-dropdown-content', component(Content));
159
- customElements.define('cosmoz-dropdown', component(Dropdown));
160
- customElements.define('cosmoz-dropdown-list', component(List));
161
- customElements.define('cosmoz-dropdown-menu', component(Menu));
162
-
163
- export { Dropdown, Content };
@@ -1,37 +0,0 @@
1
- /**
2
- * Returns all ancestor ShadowRoot elements
3
- * @param {HTMLElement} el The element
4
- * @param {HTMLElement} limit The limit to stop searching
5
- * @returns {ShadowRoot[]} Array of ancestor roots
6
- */
7
- const ancestorRoots = (el, limit = document.body) => {
8
- const roots = [];
9
- let ancestor = el;
10
- while (ancestor && ancestor !== limit) {
11
- if (ancestor.assignedSlot) {
12
- ancestor = ancestor.assignedSlot;
13
- continue;
14
- }
15
- if (ancestor instanceof ShadowRoot) {
16
- roots.push(ancestor);
17
- ancestor = ancestor.host;
18
- continue;
19
- }
20
- ancestor = ancestor.parentNode;
21
- }
22
- return roots;
23
- },
24
- /**
25
- * Listen to a scroll event in a ancestor root
26
- * @param {HTMLElement} el The element
27
- * @param {function} handler The event handler
28
- * @returns {function} Function to cleanup the event listeners
29
- */
30
- onScrolled = (el, handler) => {
31
- const roots = ancestorRoots(el);
32
- roots.forEach(r => r.addEventListener('scroll', handler, true));
33
- return () => {
34
- roots.forEach(r => r.removeEventListener('scroll', handler, true));
35
- };
36
- };
37
- export { onScrolled };
package/src/use-focus.js DELETED
@@ -1,71 +0,0 @@
1
- import { useEffect, useState, useCallback } from 'haunted';
2
- import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
3
-
4
- const isFocused = t => t.matches(':focus-within');
5
-
6
- export const useFocus = ({ disabled, onFocus }) => {
7
- const [{ focused, closed } = {}, setState] = useState(),
8
- active = focused && !disabled,
9
- meta = useMeta({ closed, onFocus }),
10
- setClosed = useCallback(
11
- closed => setState(p => ({ ...p, closed })),
12
- []
13
- ),
14
- onToggle = useCallback(e => {
15
- const target = e.currentTarget;
16
- return isFocused(target)
17
- ? setState(p => ({ focused: true, closed: !p?.closed }))
18
- : target.focus();
19
- }, []);
20
-
21
- useEffect(() => {
22
- if (!active) {
23
- return;
24
- }
25
- const handler = e => {
26
- if (e.defaultPrevented) {
27
- return;
28
- }
29
- const { closed } = meta;
30
- if (e.key === 'Escape' && !closed) {
31
- e.preventDefault();
32
- setClosed(true);
33
- } else if (['ArrowUp', 'Up'].includes(e.key) && closed) {
34
- e.preventDefault();
35
- setClosed(false);
36
- }
37
- };
38
- document.addEventListener('keydown', handler, true);
39
- return () => document.removeEventListener('keydown', handler, true);
40
- }, [active]);
41
-
42
- return {
43
- active: active && !closed,
44
- setClosed,
45
- onToggle,
46
- onFocus: useCallback(
47
- e => {
48
- const focused = isFocused(e.currentTarget);
49
- setState({ focused });
50
- meta.onFocus?.(focused);
51
- },
52
- [meta]
53
- )
54
- };
55
- };
56
-
57
- const fevs = ['focusin', 'focusout'];
58
- export const useHostFocus = host => {
59
- const thru = useFocus(host),
60
- { onFocus } = thru;
61
-
62
- useEffect(() => {
63
- host.setAttribute('tabindex', '-1');
64
- fevs.forEach(ev => host.addEventListener(ev, onFocus));
65
- return () => {
66
- fevs.forEach(ev => host.removeEventListener(ev, onFocus));
67
- };
68
- }, []);
69
-
70
- return thru;
71
- };
@@ -1,51 +0,0 @@
1
- import { useEffect } from 'haunted';
2
- import getPosition from 'position.js';
3
- import { onScrolled } from './on-scrolled';
4
-
5
- const defaultPlacement = ['bottom-left', 'bottom-right', 'bottom', 'top-left', 'top-right', 'top'],
6
- position = ({ host, anchor, placement = defaultPlacement, confinement, limit }) => {
7
- const anchorBounds = anchor.getBoundingClientRect(),
8
- hostBounds = host.getBoundingClientRect(),
9
- { popupOffset: offset } = getPosition(hostBounds, anchorBounds, placement, {
10
- fixed: true,
11
- adjustXY: 'both',
12
- offsetParent: confinement
13
- }),
14
- { style } = host;
15
- style.left = offset.left + 'px';
16
- style.top = offset.top + 'px';
17
- if (limit) {
18
- style.minWidth = Math.max(anchorBounds.width, hostBounds.width) + 'px';
19
- }
20
- },
21
- usePosition = ({ anchor: anchorage, host, ...thru }) => {
22
- useEffect(() => {
23
- const anchor = typeof anchorage === 'function' ? anchorage() : anchorage;
24
- if (anchor == null) {
25
- return;
26
- }
27
- let rid;
28
- const reposition = () => position({ host, anchor, ...thru }),
29
- ro = new ResizeObserver(reposition);
30
-
31
- ro.observe(host);
32
- ro.observe(anchor);
33
-
34
- const onReposition = () => {
35
- cancelAnimationFrame(rid);
36
- rid = requestAnimationFrame(reposition);
37
- },
38
- offScroll = onScrolled(anchor, onReposition);
39
- window.addEventListener('resize', onReposition, true);
40
-
41
- return () => {
42
- ro.unobserve(host);
43
- ro.unobserve(anchor);
44
- offScroll();
45
- window.removeEventListener('resize', onReposition, true);
46
- cancelAnimationFrame(rid);
47
- };
48
- }, [anchorage, ...Object.values(thru)]);
49
- };
50
-
51
- export { usePosition, defaultPlacement };
File without changes