@oscarpalmer/oui 0.22.0 → 0.23.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.
@@ -1,26 +1,18 @@
1
1
  //#region src/js/internal/floatable.d.ts
2
2
  declare class Floatable {
3
- readonly options: Options;
4
- active: boolean;
5
- disabled: boolean;
6
- frame: DOMHighResTimeStamp | undefined;
7
- ignoreFocus: boolean;
8
- constructor(options: Options);
9
- destroy(): void;
10
- disable(): void;
11
- enable(): void;
12
- toggle(active: boolean, ignoreFocus?: boolean): void;
13
- }
14
- type Options = {
15
3
  anchor: HTMLElement;
16
4
  content: HTMLElement;
17
- defaultPosition: Position;
18
- interactive: boolean;
19
- positionAttribute: string;
20
- reusable: boolean;
21
- onAfter?: (active: boolean) => void;
5
+ options: FloatableOptions;
6
+ constructor(anchor: HTMLElement, content: HTMLElement, options: FloatableOptions);
7
+ destroy(): void;
8
+ update(): void;
9
+ }
10
+ type FloatableOptions = {
11
+ attribute: string;
12
+ position: FloatablePosition;
22
13
  };
23
- type Position = 'above' | 'above-end' | 'above-start' | 'below' | 'below-end' | 'below-start' | 'end' | 'end-bottom' | 'end-top' | 'start' | 'start-bottom' | 'start-top';
24
- declare function deactivate(floatable: Floatable): void;
14
+ type FloatablePosition = 'above' | 'above-end' | 'above-start' | 'below' | 'below-end' | 'below-start' | 'end' | 'end-bottom' | 'end-top' | 'start' | 'start-bottom' | 'start-top';
15
+ declare function createFloatable(anchor: HTMLElement | null, content: HTMLElement, options: FloatableOptions): Floatable | undefined;
16
+ declare function removeFloatable(content: HTMLElement): void;
25
17
  //#endregion
26
- export { Floatable, deactivate };
18
+ export { Floatable, FloatableOptions, FloatablePosition, createFloatable, removeFloatable };
@@ -1,148 +1,49 @@
1
- import { on } from "@oscarpalmer/toretto/event";
2
- import { findAncestor } from "@oscarpalmer/toretto/find";
3
1
  //#region src/js/internal/floatable.ts
4
2
  var Floatable = class {
3
+ anchor;
4
+ content;
5
5
  options;
6
- active = false;
7
- disabled = false;
8
- frame;
9
- ignoreFocus = false;
10
- constructor(options) {
6
+ constructor(anchor, content, options) {
7
+ this.anchor = anchor;
8
+ this.content = content;
11
9
  this.options = options;
12
- initialize(this);
10
+ const anchorName = getAnchorName(anchor);
11
+ anchor.style.anchorName = anchorName;
12
+ content.style.positionAnchor = anchorName;
13
13
  }
14
14
  destroy() {
15
- stop(this);
16
- const state = getState();
17
- state?.active.delete(this);
18
- state?.mapped.delete(this.options.anchor);
19
- state?.mapped.delete(this.options.content);
20
- this.options.anchor = void 0;
21
- this.options.content = void 0;
15
+ this.anchor = null;
16
+ this.content = null;
22
17
  }
23
- disable() {
24
- if (this.disabled) return;
25
- this.disabled = true;
26
- stop(this);
27
- getState()?.active.delete(this);
28
- }
29
- enable() {
30
- if (!this.disabled) return;
31
- this.disabled = false;
32
- initialize(this);
33
- }
34
- toggle(active, ignoreFocus) {
35
- if (this.disabled || this.active === active) return;
36
- const state = getState();
37
- if (state == null) return;
38
- this.ignoreFocus = !active && (ignoreFocus ?? false);
39
- stop(this);
40
- if (active) state.active.add(this);
41
- else state.active.delete(this);
42
- if (this.options.interactive) if (active) state.order.push(this);
43
- else {
44
- const index = state.order.indexOf(this);
45
- if (index > -1) state.order.splice(index, 1);
46
- }
47
- if (active) activate(this);
48
- else deactivate(this);
18
+ update() {
19
+ setPosition(this);
49
20
  }
50
21
  };
51
- function activate(floatable) {
52
- floatable.active = true;
53
- document.body.append(floatable.options.content);
54
- floatable.options.content.hidden = false;
55
- setPosition(floatable);
56
- }
57
- function closeAbove(element) {
58
- const state = getState();
59
- if (state == null) return;
60
- const index = state.order.findIndex((floatable) => floatable.options.anchor === element || floatable.options.content === element);
61
- if (index === -1 || index === state.order.length - 1) return;
62
- while (index < state.order.length - 1) state.order.pop()?.toggle(false, true);
63
- }
64
- function closeAll() {
65
- const state = getState();
66
- const floatables = [...state?.active ?? []];
67
- state?.active.clear();
68
- for (const floatable of floatables) floatable.toggle(false, true);
69
- }
70
- function closeNonInteractive(state) {
71
- for (const floatable of state.active) if (!floatable.options.interactive) floatable.toggle(false);
72
- }
73
- function deactivate(floatable) {
74
- floatable.active = false;
75
- if (!floatable.options.reusable) {
76
- floatable.options.content.hidden = true;
77
- floatable.options.anchor.insertAdjacentElement(INSERT_AFTEREND, floatable.options.content);
22
+ function createFloatable(anchor, content, options) {
23
+ if (anchor == null) return;
24
+ let floatable = instances.get(content);
25
+ if (floatable == null) {
26
+ floatable = new Floatable(anchor, content, options);
27
+ instances.set(content, floatable);
78
28
  }
79
- floatable.options.onAfter?.(false);
29
+ return floatable;
80
30
  }
81
- function getAnchorName() {
31
+ function getAnchorName(anchor) {
32
+ let { anchorName } = anchor.style;
33
+ if (anchorName.length > 0) return anchorName;
82
34
  index += 1;
83
35
  return `--oui-floatable-anchor-${index}`;
84
36
  }
85
- function getState() {
86
- return window._oscarpalmer_oui_floatable;
87
- }
88
- function initialize(floatable) {
89
- const { anchor, content, interactive } = floatable.options;
90
- if (interactive) {
91
- anchor.setAttribute(ATTRIBUTE_ANCHOR, "");
92
- content.setAttribute(ATTRIBUTE_CONTENT, "");
93
- }
94
- const anchorName = getAnchorName();
95
- anchor.style.anchorName = anchorName;
96
- content.style.position = "absolute";
97
- content.style.positionAnchor = anchorName;
98
- content.style.positionTry = "normal flip-start";
99
- const state = getState();
100
- state?.mapped.set(anchor, floatable);
101
- state?.mapped.set(content, floatable);
102
- }
103
- function isDisabled(element) {
104
- return element.disabled || element.readOnly || element.getAttribute("aria-disabled") === "true";
105
- }
106
- function onClick(event) {
107
- const related = findAncestor(event, SELECTOR_ALL);
108
- if (!(related instanceof HTMLElement)) closeAll();
109
- else if (related.hasAttribute(ATTRIBUTE_CONTENT)) closeAbove(related);
110
- else if (!isDisabled(related)) toggle(related);
111
- }
112
- function onKeyDown(event) {
113
- if (event.key !== "Escape") return;
114
- const state = getState();
115
- if (state == null || state.active.size === 0) return;
116
- closeNonInteractive(state);
117
- const related = findAncestor(event, SELECTOR_CONTENT);
118
- if (!(related instanceof HTMLElement)) return;
119
- closeAbove(related);
120
- const floatable = state.mapped.get(related);
121
- if (floatable != null) floatable.toggle(false);
37
+ function removeFloatable(content) {
38
+ instances.get(content)?.destroy();
39
+ instances.delete(content);
122
40
  }
123
41
  function setPosition(floatable) {
124
- let position = floatable.options.anchor.getAttribute(floatable.options.positionAttribute) ?? floatable.options.defaultPosition;
125
- if (AREAS[position] == null) position = floatable.options.defaultPosition;
42
+ let position = floatable.anchor.getAttribute(floatable.options.attribute) ?? floatable.options.position;
43
+ if (AREAS[position] == null) position = floatable.options.position;
126
44
  const area = AREAS[position];
127
- floatable.options.content.style.positionArea = area;
128
- floatable.options.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
129
- floatable.options.onAfter?.(true);
130
- }
131
- function stop(floatable) {
132
- if (floatable.frame != null) cancelAnimationFrame(floatable.frame);
133
- floatable.frame = void 0;
134
- }
135
- function toggle(anchor) {
136
- const floatable = getState()?.mapped.get(anchor);
137
- if (floatable == null) {
138
- closeAll();
139
- return;
140
- }
141
- const active = Boolean(floatable.active);
142
- const content = findAncestor(floatable.options.anchor, SELECTOR_CONTENT);
143
- if (content == null) closeAll();
144
- else closeAbove(content);
145
- floatable.toggle(!active);
45
+ floatable.content.style.positionArea = area;
46
+ floatable.content.setAttribute(ATTRIBUTE, POSITIONS[area]);
146
47
  }
147
48
  const AREAS = {
148
49
  above: "block-start",
@@ -158,30 +59,9 @@ const AREAS = {
158
59
  "start-bottom": "span-start start",
159
60
  "start-top": "span-end start"
160
61
  };
161
- const ATTRIBUTE_ANCHOR = "oui-floatable-anchor";
162
- const ATTRIBUTE_CONTENT = "oui-floatable-content";
163
- const ATTRIBUTE_POSITION = "oui-position";
164
- const INSERT_AFTEREND = "afterend";
62
+ const ATTRIBUTE = "oui-position";
165
63
  const POSITIONS = Object.fromEntries(Object.entries(AREAS).map(([position, area]) => [area, position]));
166
- const SELECTOR_ALL = `[${ATTRIBUTE_ANCHOR}], [${ATTRIBUTE_CONTENT}]`;
167
- const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
64
+ const instances = /* @__PURE__ */ new WeakMap();
168
65
  let index = 0;
169
- if (window._oscarpalmer_oui_floatable == null) {
170
- Object.defineProperty(window, "_oscarpalmer_oui_floatable", {
171
- enumerable: false,
172
- value: {
173
- active: /* @__PURE__ */ new Set(),
174
- mapped: /* @__PURE__ */ new Map(),
175
- order: []
176
- },
177
- writable: false
178
- });
179
- on(document, "keydown", (event) => {
180
- onKeyDown(event);
181
- });
182
- on(document, "click", (event) => {
183
- onClick(event);
184
- });
185
- }
186
66
  //#endregion
187
- export { Floatable, deactivate };
67
+ export { Floatable, createFloatable, removeFloatable };
@@ -1,20 +1 @@
1
- //#region src/js/popover.d.ts
2
- declare global {
3
- interface HTMLElementTagNameMap {
4
- 'oui-popover': OuiPopoverElement;
5
- }
6
- }
7
- declare class OuiPopoverElement extends HTMLElement {
8
- #private;
9
- constructor();
10
- close(): void;
11
- connectedCallback(): void;
12
- disconnectedCallback(): void;
13
- hidePopover(): void;
14
- open(): void;
15
- showPopover(): void;
16
- toggle(): void;
17
- togglePopover(options?: boolean): boolean;
18
- }
19
- //#endregion
20
- export { OuiPopoverElement };
1
+ export { };
@@ -1,89 +1,60 @@
1
- import { createFocusTrap } from "./focus-trap/embedded.mjs";
2
- import { Floatable } from "./internal/floatable.mjs";
3
- import { getFocusable } from "@oscarpalmer/toretto/focusable";
4
- import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
1
+ import { attributable } from "./internal/attributable.mjs";
2
+ import { createFocusTrap, removeFocusTrap } from "./focus-trap/embedded.mjs";
3
+ import { createFloatable, removeFloatable } from "./internal/floatable.mjs";
4
+ import { on } from "@oscarpalmer/toretto/event";
5
5
  //#region src/js/popover.ts
6
- let index = 0;
7
- var OuiPopoverElement = class extends HTMLElement {
8
- #floatable;
9
- #focusTrap;
10
- constructor() {
11
- super();
12
- const content = this.querySelector(SELECTOR_CONTENT);
13
- const toggle = this.querySelector(SELECTOR_TOGGLE);
14
- if (!(content instanceof HTMLElement)) throw new Error("A oui-popover must have a direct child element with the [oui-popover-content]-attribute");
15
- if (!(toggle instanceof HTMLButtonElement)) throw new Error("A oui-popover must have a direct child element with the [oui-popover-toggle]-attribute and be a button");
16
- content.hidden = true;
17
- content.setAttribute("role", "dialog");
18
- content.setAttribute("aria-modal", "true");
19
- if (isNullableOrWhitespace(content.id)) content.setAttribute("id", `${ATTRIBUTE_ID_PREFIX}_${++index}`);
20
- toggle.setAttribute("aria-controls", content.id);
21
- toggle.setAttribute("aria-haspopup", "dialog");
22
- this.#floatable = new Floatable({
23
- content,
24
- anchor: toggle,
25
- defaultPosition: "below-start",
26
- interactive: true,
27
- positionAttribute: "position",
28
- reusable: false,
29
- onAfter: (active) => {
30
- if (active) focus(content);
31
- else if (!this.#floatable.ignoreFocus) toggle.focus();
32
- }
6
+ var Popover = class {
7
+ element;
8
+ anchor;
9
+ floatable;
10
+ focusTrap;
11
+ listener;
12
+ constructor(element) {
13
+ this.element = element;
14
+ this.anchor = element.ownerDocument.querySelector(`[popovertarget="${element.id}"]`);
15
+ this.floatable = createFloatable(this.anchor, element, options);
16
+ this.focusTrap = createFocusTrap(element, { noescape: true });
17
+ this.listener = on(element, EVENT_TOGGLE, (event) => {
18
+ if (event.newState === STATE_OPEN) {
19
+ ignoreFocus = false;
20
+ this.floatable?.update();
21
+ element.focus();
22
+ } else if (!ignoreFocus) this.anchor?.focus();
33
23
  });
34
- this.#focusTrap = createFocusTrap(content, { noescape: true });
35
- POPOVERS.set(toggle, this.#floatable);
36
- POPOVERS.set(content, this.#floatable);
24
+ this.element.setAttribute(ATTRIBUTE_CONTENT, "");
25
+ this.floatable?.update();
37
26
  }
38
- close() {
39
- this.hidePopover();
40
- }
41
- connectedCallback() {
42
- this.#floatable.enable();
43
- this.#focusTrap.connect();
44
- }
45
- disconnectedCallback() {
46
- this.#floatable.disable();
47
- this.#focusTrap.disconnect();
48
- }
49
- hidePopover() {
50
- if (this.#floatable.active && this.isConnected) this.#floatable.toggle(false);
51
- }
52
- open() {
53
- this.showPopover();
54
- }
55
- showPopover() {
56
- if (!this.#floatable.active && this.isConnected) this.#floatable.toggle(true);
57
- }
58
- toggle() {
59
- this.togglePopover();
60
- }
61
- togglePopover(options) {
62
- if (typeof options === "boolean") {
63
- if (options === true) this.showPopover();
64
- else this.hidePopover();
65
- return options;
66
- }
67
- if (this.#floatable.active) {
68
- this.hidePopover();
69
- return false;
70
- }
71
- this.showPopover();
72
- return true;
27
+ destroy() {
28
+ this.focusTrap?.destroy();
29
+ removeFloatable(this.element);
30
+ removeFocusTrap(this.element);
31
+ this.element.removeAttribute(ATTRIBUTE_CONTENT);
32
+ this.anchor = null;
33
+ this.element = null;
73
34
  }
74
35
  };
75
- function focus(content) {
76
- let target = getFocusable(content)[0];
77
- if (!(target instanceof HTMLElement)) target = content;
78
- requestAnimationFrame(() => {
79
- target.focus();
80
- });
36
+ function onAdd(element) {
37
+ if (!instances.has(element) && element.getAttribute(ATTRIBUTE) !== POPOVER_HINT) instances.set(element, new Popover(element));
38
+ }
39
+ function onPointerdown() {
40
+ ignoreFocus = true;
81
41
  }
82
- const ATTRIBUTE_ID_PREFIX = "oui_popover_content_";
83
- const POPOVERS = /* @__PURE__ */ new Map();
84
- const SELECTOR = "oui-popover";
85
- const SELECTOR_CONTENT = `:scope > [${SELECTOR}-content]`;
86
- const SELECTOR_TOGGLE = `:scope > [${SELECTOR}-toggle]`;
87
- customElements.define(SELECTOR, OuiPopoverElement);
42
+ function onRemove(element) {
43
+ instances.get(element)?.destroy();
44
+ instances.delete(element);
45
+ }
46
+ const ATTRIBUTE = "popover";
47
+ const ATTRIBUTE_CONTENT = "oui-popover-content";
48
+ const EVENT_TOGGLE = "toggle";
49
+ const POPOVER_HINT = "hint";
50
+ const STATE_OPEN = "open";
51
+ const instances = /* @__PURE__ */ new WeakMap();
52
+ const options = {
53
+ attribute: "position",
54
+ position: "below-start"
55
+ };
56
+ let ignoreFocus = false;
57
+ on(document, "pointerdown", onPointerdown, { capture: true });
58
+ attributable(ATTRIBUTE, onAdd, onRemove);
88
59
  //#endregion
89
- export { OuiPopoverElement };
60
+ export {};
@@ -1,4 +1,4 @@
1
1
  //#region src/js/tooltip.d.ts
2
- declare function setTooltipDelay(open?: number, close?: number): void;
2
+ declare function setTooltipDelay(value: number): void;
3
3
  //#endregion
4
4
  export { setTooltipDelay };