@oscarpalmer/oui 0.13.0 → 0.15.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.
Files changed (38) hide show
  1. package/README.md +2 -0
  2. package/dist/css/popover.css +9 -0
  3. package/dist/css/tooltip.css +9 -3
  4. package/dist/css/value.css +1 -0
  5. package/dist/js/floatable.cjs +134 -0
  6. package/dist/js/floatable.js +134 -0
  7. package/dist/js/focus-trap/embedded.cjs +150 -0
  8. package/dist/js/focus-trap/embedded.js +150 -0
  9. package/dist/js/focus-trap/standalone.cjs +8 -0
  10. package/dist/js/focus-trap/standalone.js +7 -0
  11. package/dist/js/index.cjs +5 -1
  12. package/dist/js/index.js +6 -1
  13. package/dist/js/popover.cjs +160 -0
  14. package/dist/js/popover.js +160 -0
  15. package/dist/js/tooltip.cjs +83 -126
  16. package/dist/js/tooltip.js +83 -126
  17. package/package.json +7 -7
  18. package/src/css/popover.scss +9 -0
  19. package/src/css/tooltip.scss +7 -3
  20. package/src/js/floatable.ts +196 -0
  21. package/src/js/focus-trap/embedded.ts +215 -0
  22. package/src/js/focus-trap/standalone.ts +9 -0
  23. package/src/js/index.ts +3 -1
  24. package/src/js/popover.ts +221 -0
  25. package/src/js/tooltip.ts +97 -161
  26. package/types/floatable.d.cts +16 -0
  27. package/types/floatable.d.ts +12 -0
  28. package/types/focus-trap/embedded.d.cts +15 -0
  29. package/types/focus-trap/embedded.d.ts +11 -0
  30. package/types/index.d.cts +11 -0
  31. package/types/index.d.ts +3 -1
  32. package/types/popover.d.cts +14 -0
  33. package/types/popover.d.ts +15 -0
  34. package/dist/js/focus-trap.cjs +0 -104
  35. package/dist/js/focus-trap.js +0 -103
  36. package/src/js/focus-trap.ts +0 -133
  37. /package/types/{focus-trap.d.cts → focus-trap/standalone.d.cts} +0 -0
  38. /package/types/{focus-trap.d.ts → focus-trap/standalone.d.ts} +0 -0
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const is = require("@oscarpalmer/atoms/is");
4
+ const event = require("@oscarpalmer/toretto/event");
5
+ const find = require("@oscarpalmer/toretto/find");
6
+ const focusable = require("@oscarpalmer/toretto/focusable");
7
+ const floatable = require("./floatable.cjs");
8
+ const focusTrap_embedded = require("./focus-trap/embedded.cjs");
9
+ const mapped = /* @__PURE__ */ new Map();
10
+ let index = 0;
11
+ class OuiPopoverElement extends HTMLElement {
12
+ #floatable;
13
+ #focusTrap;
14
+ constructor() {
15
+ super();
16
+ const content = this.querySelector(":scope > [oui-popover-content]");
17
+ const toggle2 = this.querySelector(":scope > [oui-popover-toggle]");
18
+ if (!(content instanceof HTMLElement)) {
19
+ throw new Error(
20
+ "A oui-popover must have a direct child element with the [oui-popover-content]-attribute"
21
+ );
22
+ }
23
+ if (!(toggle2 instanceof HTMLButtonElement)) {
24
+ throw new Error(
25
+ "A oui-popover must have a direct child element with the [oui-popover-toggle]-attribute and be a button"
26
+ );
27
+ }
28
+ content.hidden = true;
29
+ content.setAttribute("role", "dialog");
30
+ content.setAttribute("oui-focus-trap", "");
31
+ content.setAttribute("oui-focus-trap-noescape", "");
32
+ if (is.isNullableOrWhitespace(content.id)) {
33
+ content.setAttribute("id", `oui-popover-content-${++index}`);
34
+ }
35
+ toggle2.setAttribute("aria-controls", content.id);
36
+ toggle2.setAttribute("aria-haspopup", "dialog");
37
+ this.#floatable = new floatable.Floatable(toggle2, content);
38
+ this.#focusTrap = focusTrap_embedded.createFocusTrap(content);
39
+ mapped.set(toggle2, this.#floatable);
40
+ mapped.set(content, this.#floatable);
41
+ }
42
+ close() {
43
+ this.hidePopover();
44
+ }
45
+ hidePopover() {
46
+ if (this.#floatable.active) {
47
+ closeAbove(this.#floatable.content);
48
+ floatable.deactivate(this.#floatable, active, order);
49
+ this.#floatable.anchor.focus();
50
+ }
51
+ }
52
+ open() {
53
+ this.showPopover();
54
+ }
55
+ showPopover() {
56
+ if (!this.#floatable.active) {
57
+ toggle(this.#floatable.anchor);
58
+ }
59
+ }
60
+ toggle() {
61
+ this.togglePopover();
62
+ }
63
+ togglePopover(options) {
64
+ if (typeof options === "boolean") {
65
+ if (options === true) {
66
+ this.showPopover();
67
+ } else {
68
+ this.hidePopover();
69
+ }
70
+ return options;
71
+ }
72
+ if (this.#floatable.active) {
73
+ this.hidePopover();
74
+ return false;
75
+ }
76
+ this.showPopover();
77
+ return true;
78
+ }
79
+ }
80
+ function closeAll() {
81
+ for (const floatable$1 of active) {
82
+ floatable.deactivate(floatable$1, active, order);
83
+ }
84
+ }
85
+ function closeAbove(element) {
86
+ const index2 = order.findIndex(
87
+ (floatable2) => floatable2.anchor === element || floatable2.content === element
88
+ );
89
+ if (index2 === -1 || index2 === order.length - 1) {
90
+ return;
91
+ }
92
+ while (index2 < order.length - 1) {
93
+ const floatable$1 = order.pop();
94
+ if (floatable$1 != null) {
95
+ floatable.deactivate(floatable$1, active, order);
96
+ }
97
+ }
98
+ order[index2].content.focus();
99
+ }
100
+ function focus(content) {
101
+ let target = focusable.getFocusable(content)[0];
102
+ if (!(target instanceof HTMLElement)) {
103
+ target = content;
104
+ }
105
+ requestAnimationFrame(() => {
106
+ target.focus();
107
+ });
108
+ }
109
+ function onClick(event2) {
110
+ const related = find.findAncestor(
111
+ event2.target,
112
+ "[oui-popover-toggle], [oui-popover-content]"
113
+ );
114
+ if (!(related instanceof HTMLElement)) {
115
+ closeAll();
116
+ } else if (related.hasAttribute("oui-popover-content")) {
117
+ closeAbove(related);
118
+ } else {
119
+ toggle(related);
120
+ }
121
+ }
122
+ function onKeydown(event2) {
123
+ if (event2.key !== "Escape" || order.length === 0) {
124
+ return;
125
+ }
126
+ const related = find.findAncestor(event2.target, "[oui-popover-content]");
127
+ if (!(related instanceof HTMLElement)) {
128
+ return;
129
+ }
130
+ closeAbove(related);
131
+ const floatable$1 = mapped.get(related);
132
+ if (floatable$1 != null) {
133
+ floatable.deactivate(floatable$1, active, order);
134
+ floatable$1.anchor.focus();
135
+ }
136
+ }
137
+ function toggle(anchor) {
138
+ const content = find.findAncestor(anchor, "[oui-popover-content]");
139
+ if (content instanceof HTMLElement) {
140
+ closeAbove(content);
141
+ } else {
142
+ closeAll();
143
+ }
144
+ const floatable$1 = mapped.get(anchor);
145
+ if (floatable$1 == null) {
146
+ return;
147
+ }
148
+ if (floatable$1.active) {
149
+ floatable.deactivate(floatable$1, active, order);
150
+ } else {
151
+ floatable.activate(floatable$1, active, order);
152
+ focus(floatable$1.content);
153
+ }
154
+ }
155
+ customElements.define("oui-popover", OuiPopoverElement);
156
+ event.on(document, "click", onClick);
157
+ event.on(document, "keydown", onKeydown);
158
+ const active = /* @__PURE__ */ new Set();
159
+ const order = [];
160
+ exports.OuiPopoverElement = OuiPopoverElement;
@@ -0,0 +1,160 @@
1
+ import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
2
+ import { on } from "@oscarpalmer/toretto/event";
3
+ import { findAncestor } from "@oscarpalmer/toretto/find";
4
+ import { getFocusable } from "@oscarpalmer/toretto/focusable";
5
+ import { Floatable, deactivate, activate } from "./floatable.js";
6
+ import { createFocusTrap } from "./focus-trap/embedded.js";
7
+ const mapped = /* @__PURE__ */ new Map();
8
+ let index = 0;
9
+ class OuiPopoverElement extends HTMLElement {
10
+ #floatable;
11
+ #focusTrap;
12
+ constructor() {
13
+ super();
14
+ const content = this.querySelector(":scope > [oui-popover-content]");
15
+ const toggle2 = this.querySelector(":scope > [oui-popover-toggle]");
16
+ if (!(content instanceof HTMLElement)) {
17
+ throw new Error(
18
+ "A oui-popover must have a direct child element with the [oui-popover-content]-attribute"
19
+ );
20
+ }
21
+ if (!(toggle2 instanceof HTMLButtonElement)) {
22
+ throw new Error(
23
+ "A oui-popover must have a direct child element with the [oui-popover-toggle]-attribute and be a button"
24
+ );
25
+ }
26
+ content.hidden = true;
27
+ content.setAttribute("role", "dialog");
28
+ content.setAttribute("oui-focus-trap", "");
29
+ content.setAttribute("oui-focus-trap-noescape", "");
30
+ if (isNullableOrWhitespace(content.id)) {
31
+ content.setAttribute("id", `oui-popover-content-${++index}`);
32
+ }
33
+ toggle2.setAttribute("aria-controls", content.id);
34
+ toggle2.setAttribute("aria-haspopup", "dialog");
35
+ this.#floatable = new Floatable(toggle2, content);
36
+ this.#focusTrap = createFocusTrap(content);
37
+ mapped.set(toggle2, this.#floatable);
38
+ mapped.set(content, this.#floatable);
39
+ }
40
+ close() {
41
+ this.hidePopover();
42
+ }
43
+ hidePopover() {
44
+ if (this.#floatable.active) {
45
+ closeAbove(this.#floatable.content);
46
+ deactivate(this.#floatable, active, order);
47
+ this.#floatable.anchor.focus();
48
+ }
49
+ }
50
+ open() {
51
+ this.showPopover();
52
+ }
53
+ showPopover() {
54
+ if (!this.#floatable.active) {
55
+ toggle(this.#floatable.anchor);
56
+ }
57
+ }
58
+ toggle() {
59
+ this.togglePopover();
60
+ }
61
+ togglePopover(options) {
62
+ if (typeof options === "boolean") {
63
+ if (options === true) {
64
+ this.showPopover();
65
+ } else {
66
+ this.hidePopover();
67
+ }
68
+ return options;
69
+ }
70
+ if (this.#floatable.active) {
71
+ this.hidePopover();
72
+ return false;
73
+ }
74
+ this.showPopover();
75
+ return true;
76
+ }
77
+ }
78
+ function closeAll() {
79
+ for (const floatable of active) {
80
+ deactivate(floatable, active, order);
81
+ }
82
+ }
83
+ function closeAbove(element) {
84
+ const index2 = order.findIndex(
85
+ (floatable) => floatable.anchor === element || floatable.content === element
86
+ );
87
+ if (index2 === -1 || index2 === order.length - 1) {
88
+ return;
89
+ }
90
+ while (index2 < order.length - 1) {
91
+ const floatable = order.pop();
92
+ if (floatable != null) {
93
+ deactivate(floatable, active, order);
94
+ }
95
+ }
96
+ order[index2].content.focus();
97
+ }
98
+ function focus(content) {
99
+ let target = getFocusable(content)[0];
100
+ if (!(target instanceof HTMLElement)) {
101
+ target = content;
102
+ }
103
+ requestAnimationFrame(() => {
104
+ target.focus();
105
+ });
106
+ }
107
+ function onClick(event) {
108
+ const related = findAncestor(
109
+ event.target,
110
+ "[oui-popover-toggle], [oui-popover-content]"
111
+ );
112
+ if (!(related instanceof HTMLElement)) {
113
+ closeAll();
114
+ } else if (related.hasAttribute("oui-popover-content")) {
115
+ closeAbove(related);
116
+ } else {
117
+ toggle(related);
118
+ }
119
+ }
120
+ function onKeydown(event) {
121
+ if (event.key !== "Escape" || order.length === 0) {
122
+ return;
123
+ }
124
+ const related = findAncestor(event.target, "[oui-popover-content]");
125
+ if (!(related instanceof HTMLElement)) {
126
+ return;
127
+ }
128
+ closeAbove(related);
129
+ const floatable = mapped.get(related);
130
+ if (floatable != null) {
131
+ deactivate(floatable, active, order);
132
+ floatable.anchor.focus();
133
+ }
134
+ }
135
+ function toggle(anchor) {
136
+ const content = findAncestor(anchor, "[oui-popover-content]");
137
+ if (content instanceof HTMLElement) {
138
+ closeAbove(content);
139
+ } else {
140
+ closeAll();
141
+ }
142
+ const floatable = mapped.get(anchor);
143
+ if (floatable == null) {
144
+ return;
145
+ }
146
+ if (floatable.active) {
147
+ deactivate(floatable, active, order);
148
+ } else {
149
+ activate(floatable, active, order);
150
+ focus(floatable.content);
151
+ }
152
+ }
153
+ customElements.define("oui-popover", OuiPopoverElement);
154
+ on(document, "click", onClick);
155
+ on(document, "keydown", onKeydown);
156
+ const active = /* @__PURE__ */ new Set();
157
+ const order = [];
158
+ export {
159
+ OuiPopoverElement
160
+ };
@@ -2,144 +2,120 @@
2
2
  const is = require("@oscarpalmer/atoms/is");
3
3
  const event = require("@oscarpalmer/toretto/event");
4
4
  const attributable = require("./attributable.cjs");
5
+ const floatable = require("./floatable.cjs");
5
6
  class Tooltip {
6
- constructor(parent) {
7
- this.parent = parent;
8
- this.element = getContent(parent);
9
- if (this.element != null) {
10
- parent.insertAdjacentElement("afterend", this.element);
11
- initialize(this);
12
- }
13
- }
14
7
  abortController = new AbortController();
15
- activate;
16
- active = false;
17
- deactivate;
18
- element;
19
- frame;
8
+ attributes;
9
+ floatable;
10
+ constructor(anchor, content) {
11
+ this.attributes = content.attributes;
12
+ this.floatable = new floatable.Floatable(anchor, content.element);
13
+ initialize(this);
14
+ }
20
15
  destroy() {
21
- deactivate(this);
16
+ floatable.deactivate(this.floatable, active);
17
+ reset(this.floatable.anchor, this.attributes);
22
18
  this.abortController.abort();
19
+ this.floatable.content.remove();
20
+ this.floatable.destroy();
23
21
  this.abortController = void 0;
24
- this.activate = void 0;
25
- this.deactivate = void 0;
26
- this.element = void 0;
27
- this.frame = void 0;
28
- this.parent = void 0;
22
+ this.attributes = void 0;
23
+ this.floatable = void 0;
29
24
  }
30
25
  }
31
- function activate(tooltip) {
32
- stop(tooltip);
33
- if (tooltip.active) {
26
+ function addTooltip(anchor) {
27
+ if (tooltips.has(anchor)) {
34
28
  return;
35
29
  }
36
- tooltip.activate = +setTimeout(() => {
37
- tooltip.active = true;
38
- active.add(tooltip);
39
- document.body.append(tooltip.element);
40
- tooltip.element.hidden = false;
41
- update(tooltip);
42
- }, 250);
43
- }
44
- function addTooltip(element) {
45
- if (!tooltips.has(element)) {
46
- tooltips.set(element, new Tooltip(element));
30
+ const content = getContent(anchor);
31
+ if (content != null) {
32
+ tooltips.set(anchor, new Tooltip(anchor, content));
47
33
  }
48
34
  }
49
- function deactivate(tooltip, time) {
50
- stop(tooltip);
51
- if (!tooltip.active) {
52
- return;
35
+ function getContent(anchor) {
36
+ const attributes = {
37
+ ariaDescribedby: anchor.getAttribute("aria-describedby"),
38
+ ariaLabelledBy: anchor.getAttribute("aria-labelledby")
39
+ };
40
+ const wrapper = getWrapper(anchor);
41
+ const labelledElements = getElements(attributes.ariaLabelledBy);
42
+ if (labelledElements.length > 0) {
43
+ anchor.setAttribute("aria-labelledby", wrapper.id);
44
+ for (const element of labelledElements) {
45
+ wrapper.append(element);
46
+ }
47
+ return {
48
+ attributes,
49
+ element: wrapper
50
+ };
53
51
  }
54
- tooltip.deactivate = +setTimeout(() => {
55
- tooltip.active = false;
56
- active.delete(tooltip);
57
- tooltip.element.hidden = true;
58
- tooltip.parent.insertAdjacentElement("afterend", tooltip.element);
59
- }, time ?? 250);
60
- }
61
- function getContent(parent) {
62
- const wrapper = getWrapper(parent);
63
- const ids = (parent.getAttribute("aria-describedby") ?? "").split(/\s+/);
64
- parent.setAttribute("aria-describedby", wrapper.id);
65
- const elements = ids.map(
66
- (id) => is.isNullableOrWhitespace(id) ? void 0 : document.querySelector(`#${id}`)
67
- ).filter((element) => element instanceof HTMLElement);
68
- if (elements.length > 0) {
69
- for (const element of elements) {
70
- element.hidden = false;
52
+ const describedElements = getElements(attributes.ariaDescribedby);
53
+ if (describedElements.length > 0) {
54
+ anchor.setAttribute("aria-describedby", wrapper.id);
55
+ for (const element of describedElements) {
71
56
  wrapper.append(element);
72
57
  }
73
- return wrapper;
58
+ return {
59
+ attributes,
60
+ element: wrapper
61
+ };
74
62
  }
75
- let content = parent.getAttribute("aria-description");
63
+ let content = anchor.getAttribute("aria-label");
76
64
  if (is.isNullableOrWhitespace(content)) {
77
- content = parent.getAttribute("aria-label");
78
- if (is.isNullableOrWhitespace(content)) {
79
- return;
80
- }
65
+ content = anchor.getAttribute("aria-description");
66
+ }
67
+ if (is.isNullableOrWhitespace(content)) {
68
+ return;
81
69
  }
82
70
  const paragraph = document.createElement("p");
83
71
  paragraph.textContent = content;
84
72
  wrapper.append(paragraph);
85
- return wrapper;
73
+ return {
74
+ attributes,
75
+ element: wrapper
76
+ };
86
77
  }
87
- function getTop(parent, content) {
88
- let top = parent.top - content.height - margin;
89
- if (top - margin >= 0) {
90
- return top;
91
- }
92
- top = parent.bottom + margin;
93
- if (top + content.height + margin <= window.innerHeight) {
94
- return top;
78
+ function getElements(attribute) {
79
+ const ids = attribute?.split(/\s+/) ?? [];
80
+ const elements = [];
81
+ for (const id of ids) {
82
+ if (is.isNullableOrWhitespace(id)) {
83
+ continue;
84
+ }
85
+ const element = document.querySelector(`#${id}`);
86
+ if (element instanceof HTMLElement) {
87
+ const cloned = element.cloneNode(true);
88
+ cloned.hidden = false;
89
+ elements.push(cloned);
90
+ }
95
91
  }
96
- return parent.top + (parent.height - content.height) / 2;
92
+ return elements;
97
93
  }
98
- function getWrapper(parent) {
94
+ function getWrapper(anchor) {
99
95
  const wrapper = document.createElement("div");
100
96
  wrapper.setAttribute(`${selector}-content`, "");
101
- wrapper.className = parent.getAttribute(`${selector}-class`) ?? "";
97
+ wrapper.className = anchor.getAttribute(`${selector}-class`) ?? "";
102
98
  wrapper.hidden = true;
103
99
  wrapper.id = `${selector}-${++index}`;
104
100
  wrapper.role = "tooltip";
105
101
  wrapper.tabIndex = -1;
106
102
  return wrapper;
107
103
  }
108
- function getX(parent, content) {
109
- if (content.width + 2 * margin >= window.innerWidth) {
110
- return {
111
- left: margin,
112
- right: margin
113
- };
114
- }
115
- const left = parent.left + (parent.width - content.width) / 2;
116
- const right = window.innerWidth - margin;
117
- if (left < margin) {
118
- return {
119
- left: margin
120
- };
121
- }
122
- if (left + content.width > right) {
123
- return {
124
- right: margin
125
- };
126
- }
127
- return {
128
- left
129
- };
130
- }
131
104
  function initialize(tooltip) {
132
- const elements = [tooltip.parent, tooltip.element];
105
+ const { abortController, floatable: floatable$1 } = tooltip;
106
+ const { anchor, content } = floatable$1;
107
+ anchor.insertAdjacentElement("afterend", content);
108
+ const elements = [anchor, content];
133
109
  for (const element of elements) {
134
110
  for (const event$1 of activateEvents) {
135
111
  event.on(
136
112
  element,
137
113
  event$1,
138
114
  () => {
139
- activate(tooltip);
115
+ floatable.activate(floatable$1, active, void 0, 250);
140
116
  },
141
117
  {
142
- signal: tooltip.abortController.signal
118
+ signal: abortController.signal
143
119
  }
144
120
  );
145
121
  }
@@ -148,10 +124,10 @@ function initialize(tooltip) {
148
124
  element,
149
125
  event$1,
150
126
  () => {
151
- deactivate(tooltip);
127
+ floatable.deactivate(floatable$1, active, void 0, 250);
152
128
  },
153
129
  {
154
- signal: tooltip.abortController.signal
130
+ signal: abortController.signal
155
131
  }
156
132
  );
157
133
  }
@@ -161,43 +137,24 @@ function removeTooltip(element) {
161
137
  tooltips.get(element)?.destroy();
162
138
  tooltips.delete(element);
163
139
  }
164
- function stop(tooltip) {
165
- if (tooltip.frame != null) {
166
- cancelAnimationFrame(tooltip.frame);
167
- tooltip.frame = void 0;
168
- }
169
- if (tooltip.activate != null) {
170
- clearTimeout(tooltip.activate);
171
- tooltip.activate = void 0;
172
- }
173
- if (tooltip.deactivate != null) {
174
- clearTimeout(tooltip.deactivate);
175
- tooltip.deactivate = void 0;
176
- }
177
- }
178
- function update(tooltip) {
179
- function run() {
180
- const parentRect = tooltip.parent.getBoundingClientRect();
181
- const contentRect = tooltip.element.getBoundingClientRect();
182
- const top = getTop(parentRect, contentRect);
183
- const { left, right } = getX(parentRect, contentRect);
184
- tooltip.element.style.inset = `${top}px ${right == null ? "auto" : `${right}px`} auto ${left == null ? "auto" : `${left}px`}`;
185
- tooltip.frame = requestAnimationFrame(run);
140
+ function reset(anchor, attributes) {
141
+ if (attributes.ariaDescribedby == null) {
142
+ anchor.removeAttribute("aria-describedby");
143
+ } else {
144
+ anchor.setAttribute("aria-describedby", attributes.ariaDescribedby);
186
145
  }
187
- tooltip.frame = requestAnimationFrame(run);
188
146
  }
189
147
  const active = /* @__PURE__ */ new Set();
190
148
  const activateEvents = ["focus", "mouseenter", "touchstart"];
191
149
  const deactivateEvents = ["blur", "mouseleave"];
192
- const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 4;
193
150
  const selector = "oui-tooltip";
194
151
  const tooltips = /* @__PURE__ */ new WeakMap();
195
152
  let index = 0;
196
153
  attributable.attributable(selector, addTooltip, removeTooltip);
197
154
  event.on(document, "keydown", (event2) => {
198
155
  if (event2.key === "Escape") {
199
- for (const tooltip of active) {
200
- deactivate(tooltip, 0);
156
+ for (const floatable$1 of active) {
157
+ floatable.deactivate(floatable$1, active);
201
158
  }
202
159
  }
203
160
  });