@oscarpalmer/oui 0.24.0 → 0.25.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.
- package/dist/css/{tooltip.css → floatable.css} +59 -24
- package/dist/css/reset.css +17 -0
- package/dist/js/dialog.d.mts +38 -0
- package/dist/js/dialog.mjs +92 -0
- package/dist/js/embedded-84gxpReS.d.mts +64 -0
- package/dist/js/embedded-BpwzMSEF.d.mts +26 -0
- package/dist/js/floatable/embedded.d.mts +2 -0
- package/dist/js/{floatable.mjs → floatable/embedded.mjs} +46 -30
- package/dist/js/floatable/standalone.d.mts +2 -0
- package/dist/js/floatable/standalone.mjs +2 -0
- package/dist/js/focus-trap/embedded.d.mts +1 -25
- package/dist/js/focus-trap/embedded.mjs +1 -1
- package/dist/js/index.d.mts +5 -3
- package/dist/js/index.mjs +7 -5
- package/dist/js/internal/draggable.mjs +1 -1
- package/dist/js/popover.d.mts +38 -1
- package/dist/js/popover.mjs +80 -41
- package/dist/js/sortable.mjs +1 -1
- package/dist/js/splitter.mjs +1 -1
- package/dist/js/tooltip.d.mts +27 -1
- package/dist/js/tooltip.mjs +82 -46
- package/package.json +2 -2
- package/src/css/{tooltip.scss → floatable.scss} +47 -5
- package/src/css/mixins.scss +0 -2
- package/src/css/reset.scss +17 -0
- package/src/js/dialog.ts +188 -0
- package/src/js/{floatable.ts → floatable/embedded.ts} +111 -46
- package/src/js/floatable/standalone.ts +1 -0
- package/src/js/index.ts +5 -4
- package/src/js/popover.ts +139 -57
- package/src/js/tooltip.ts +129 -49
- package/dist/css/popover.css +0 -30
- package/dist/js/floatable.d.mts +0 -59
- package/src/css/popover.scss +0 -10
package/src/js/dialog.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {on} from '@oscarpalmer/toretto/event';
|
|
2
|
+
import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
3
|
+
import {getOnBeforeToggleListener} from './floatable/embedded';
|
|
4
|
+
import {attributable} from './internal/attributable';
|
|
5
|
+
|
|
6
|
+
// #region Types
|
|
7
|
+
|
|
8
|
+
class OuiDialog {
|
|
9
|
+
readonly #state: OuiDialogState;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Is the dialog open?
|
|
13
|
+
*/
|
|
14
|
+
get open(): boolean {
|
|
15
|
+
return this.#state.element.open;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
constructor(state: OuiDialogState) {
|
|
19
|
+
this.#state = state;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Closes the dialog by request
|
|
24
|
+
*/
|
|
25
|
+
hide(request: true, value?: string): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Closes the dialog
|
|
29
|
+
*/
|
|
30
|
+
hide(value?: string): void;
|
|
31
|
+
|
|
32
|
+
hide(first?: string | boolean, second?: string): void {
|
|
33
|
+
if (!this.#state.element.open) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (first === true) {
|
|
38
|
+
this.#state.element.requestClose(typeof second === 'string' ? second : undefined);
|
|
39
|
+
} else {
|
|
40
|
+
this.#state.element.close(typeof first === 'string' ? first : undefined);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Opens the dialog
|
|
46
|
+
*/
|
|
47
|
+
show(modal?: boolean): void {
|
|
48
|
+
const {element} = this.#state;
|
|
49
|
+
|
|
50
|
+
if (element == null || element.open) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isModal = modal !== false;
|
|
55
|
+
|
|
56
|
+
element.setAttribute(ARIA_MODAL, String(isModal));
|
|
57
|
+
|
|
58
|
+
if (isModal) {
|
|
59
|
+
element.showModal();
|
|
60
|
+
} else {
|
|
61
|
+
element.show();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type OuiDialogState = {
|
|
67
|
+
dialog: OuiDialog;
|
|
68
|
+
element: HTMLDialogElement;
|
|
69
|
+
listeners: RemovableEventListener[];
|
|
70
|
+
modal: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// #endregion
|
|
74
|
+
|
|
75
|
+
// #region Functions
|
|
76
|
+
|
|
77
|
+
function addDialog(element: HTMLElement): void {
|
|
78
|
+
if (!(element instanceof HTMLDialogElement)) {
|
|
79
|
+
throw new TypeError(MESSAGE);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!states.has(element)) {
|
|
83
|
+
states.set(element, getState(element));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function destroyDialog(state: OuiDialogState): void {
|
|
88
|
+
state.element.close();
|
|
89
|
+
|
|
90
|
+
state.element.setAttribute(ARIA_MODAL, String(state.modal));
|
|
91
|
+
state.element.removeAttribute(ATTRIBUTE_OPEN);
|
|
92
|
+
|
|
93
|
+
for (const listener of state.listeners) {
|
|
94
|
+
listener();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
state.dialog = undefined as never;
|
|
98
|
+
state.element = undefined as never;
|
|
99
|
+
state.listeners = undefined as never;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the _OuiDialog_ for an element
|
|
104
|
+
*
|
|
105
|
+
* @param element Element to get _OuiDialog_ for
|
|
106
|
+
* @returns _OuiDialog_ instance
|
|
107
|
+
*/
|
|
108
|
+
function getDialog(element: HTMLDialogElement): OuiDialog | undefined {
|
|
109
|
+
return states.get(element)?.dialog;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getState(element: HTMLDialogElement): OuiDialogState {
|
|
113
|
+
const state: OuiDialogState = {
|
|
114
|
+
element,
|
|
115
|
+
dialog: undefined as never,
|
|
116
|
+
listeners: [
|
|
117
|
+
getOnBeforeToggleListener(element),
|
|
118
|
+
on(element, EVENT_TOGGLE, event => {
|
|
119
|
+
const {source} = event;
|
|
120
|
+
|
|
121
|
+
if (element.open) {
|
|
122
|
+
const modal =
|
|
123
|
+
(source instanceof HTMLButtonElement && source.command === COMMAND_SHOW_MODAL) ||
|
|
124
|
+
element.matches(SELECTOR_MODAL);
|
|
125
|
+
|
|
126
|
+
element.setAttribute(ARIA_MODAL, String(modal));
|
|
127
|
+
element.setAttribute(ATTRIBUTE_OPEN, '');
|
|
128
|
+
} else {
|
|
129
|
+
element.removeAttribute(ATTRIBUTE_OPEN);
|
|
130
|
+
element.setAttribute(ARIA_MODAL, String(state.modal));
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
],
|
|
134
|
+
modal: element.getAttribute(ARIA_MODAL) === TRUE,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
state.dialog = new OuiDialog(state);
|
|
138
|
+
|
|
139
|
+
return state;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function removeDialog(element: HTMLElement): void {
|
|
143
|
+
const state = states.get(element);
|
|
144
|
+
|
|
145
|
+
if (state == null) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
destroyDialog(state);
|
|
150
|
+
|
|
151
|
+
states.delete(element);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// #endregion
|
|
155
|
+
|
|
156
|
+
// #region Variables
|
|
157
|
+
|
|
158
|
+
const ARIA_MODAL = 'aria-modal';
|
|
159
|
+
|
|
160
|
+
const ATTRIBUTE = 'oui-dialog';
|
|
161
|
+
|
|
162
|
+
const ATTRIBUTE_OPEN = `${ATTRIBUTE}-open`;
|
|
163
|
+
|
|
164
|
+
const COMMAND_SHOW_MODAL = 'show-modal';
|
|
165
|
+
|
|
166
|
+
const EVENT_TOGGLE = 'toggle';
|
|
167
|
+
|
|
168
|
+
const MESSAGE = 'The element must be an instance of HTMLDialogElement';
|
|
169
|
+
|
|
170
|
+
const SELECTOR_MODAL = ':modal';
|
|
171
|
+
|
|
172
|
+
const TRUE = 'true';
|
|
173
|
+
|
|
174
|
+
const states = new WeakMap<HTMLElement, OuiDialogState>();
|
|
175
|
+
|
|
176
|
+
// #endregion
|
|
177
|
+
|
|
178
|
+
// #region Initialization
|
|
179
|
+
|
|
180
|
+
attributable(ATTRIBUTE, addDialog, removeDialog);
|
|
181
|
+
|
|
182
|
+
// #endregion
|
|
183
|
+
|
|
184
|
+
// #region Exports
|
|
185
|
+
|
|
186
|
+
export {getDialog, type OuiDialog};
|
|
187
|
+
|
|
188
|
+
// #endregion
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
2
|
+
import {on} from '@oscarpalmer/toretto/event';
|
|
2
3
|
import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
|
|
4
|
+
import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
// #region Types
|
|
7
|
+
|
|
8
|
+
export class OuiFloatable {
|
|
5
9
|
#destroyed = false;
|
|
6
10
|
|
|
11
|
+
readonly #state: OuiFloatableState;
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Is the _Floatable_ open?
|
|
9
15
|
*/
|
|
10
16
|
get open(): boolean {
|
|
11
|
-
return this.content.checkVisibility();
|
|
17
|
+
return this.#state.content.checkVisibility();
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
constructor(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
) {
|
|
20
|
+
constructor(state: OuiFloatableState, standalone: boolean) {
|
|
21
|
+
this.#state = state;
|
|
22
|
+
|
|
23
|
+
const {anchor, content} = state;
|
|
24
|
+
|
|
20
25
|
if (standalone) {
|
|
21
26
|
content.setAttribute(ATTRIBUTE_FLOATABLE, '');
|
|
22
27
|
|
|
@@ -30,7 +35,7 @@ export class Floatable {
|
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
|
-
* Destroy the
|
|
38
|
+
* Destroy the _OuiFloatable_, removing it from the DOM and cleaning up references
|
|
34
39
|
*/
|
|
35
40
|
destroy(): void {
|
|
36
41
|
if (this.#destroyed) {
|
|
@@ -39,48 +44,48 @@ export class Floatable {
|
|
|
39
44
|
|
|
40
45
|
this.#destroyed = true;
|
|
41
46
|
|
|
42
|
-
removeFloatable(this.content);
|
|
47
|
+
removeFloatable(this.#state.content);
|
|
43
48
|
|
|
44
|
-
this.anchor = null as never;
|
|
45
|
-
this.content = null as never;
|
|
49
|
+
this.#state.anchor = null as never;
|
|
50
|
+
this.#state.content = null as never;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
/**
|
|
49
|
-
*
|
|
54
|
+
* Closes the _OuiFloatable_
|
|
50
55
|
*/
|
|
51
56
|
hide(): void {
|
|
52
|
-
if (!this.#destroyed) {
|
|
53
|
-
this.content.hidePopover();
|
|
57
|
+
if (!this.#destroyed && this.open) {
|
|
58
|
+
this.#state.content.hidePopover();
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
/**
|
|
58
|
-
*
|
|
63
|
+
* Opens the _OuiFloatable_
|
|
59
64
|
*/
|
|
60
65
|
show(): void {
|
|
61
|
-
if (!this.#destroyed) {
|
|
62
|
-
this.content.showPopover();
|
|
66
|
+
if (!this.#destroyed && !this.open) {
|
|
67
|
+
this.#state.content.showPopover();
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
/**
|
|
67
|
-
* Update the position of the
|
|
72
|
+
* Update the position of the _OuiFloatable_, with an optional new position
|
|
68
73
|
*
|
|
69
74
|
* @param position Optional new position
|
|
70
75
|
*/
|
|
71
|
-
update(position?:
|
|
76
|
+
update(position?: OuiFloatablePosition): void {
|
|
72
77
|
if (!this.#destroyed) {
|
|
73
|
-
setPosition(this, position);
|
|
78
|
+
setPosition(this.#state, position);
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
export type
|
|
83
|
+
export type OuiFloatableOptions = {
|
|
79
84
|
attribute?: string;
|
|
80
|
-
position:
|
|
85
|
+
position: OuiFloatablePosition;
|
|
81
86
|
};
|
|
82
87
|
|
|
83
|
-
export type
|
|
88
|
+
export type OuiFloatablePosition =
|
|
84
89
|
| 'above'
|
|
85
90
|
| 'above-end'
|
|
86
91
|
| 'above-start'
|
|
@@ -94,23 +99,33 @@ export type FloatablePosition =
|
|
|
94
99
|
| 'start-bottom'
|
|
95
100
|
| 'start-top';
|
|
96
101
|
|
|
97
|
-
type
|
|
102
|
+
type OuiFloatableState = {
|
|
103
|
+
anchor: HTMLElement;
|
|
104
|
+
content: HTMLElement;
|
|
105
|
+
options: OuiFloatableOptions;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type GetOuiFloatableOptions = {
|
|
98
109
|
/**
|
|
99
|
-
* Open the
|
|
110
|
+
* Open the _OuiFloatable_ when created? _(defaults to `true`)_
|
|
100
111
|
*/
|
|
101
112
|
open?: boolean;
|
|
102
113
|
/**
|
|
103
|
-
* Position of the
|
|
114
|
+
* Position of the _OuiFloatable_ _(defaults to `below`)_
|
|
104
115
|
*/
|
|
105
|
-
position?:
|
|
116
|
+
position?: OuiFloatablePosition;
|
|
106
117
|
};
|
|
107
118
|
|
|
119
|
+
// #endregion
|
|
120
|
+
|
|
121
|
+
// #region Functions
|
|
122
|
+
|
|
108
123
|
export function createFloatable(
|
|
109
124
|
anchor: HTMLElement,
|
|
110
125
|
content: HTMLElement,
|
|
111
|
-
options:
|
|
126
|
+
options: OuiFloatableOptions,
|
|
112
127
|
standalone: boolean,
|
|
113
|
-
):
|
|
128
|
+
): OuiFloatable {
|
|
114
129
|
if (!isHTMLOrSVGElement(anchor) || !isHTMLOrSVGElement(content)) {
|
|
115
130
|
throw new TypeError('Anchor and content must be an HTMLElement or SVGElement.');
|
|
116
131
|
}
|
|
@@ -118,7 +133,7 @@ export function createFloatable(
|
|
|
118
133
|
let floatable = instances.get(content);
|
|
119
134
|
|
|
120
135
|
if (floatable == null) {
|
|
121
|
-
floatable = new
|
|
136
|
+
floatable = new OuiFloatable(getState(anchor, content, options), standalone);
|
|
122
137
|
|
|
123
138
|
instances.set(content, floatable);
|
|
124
139
|
}
|
|
@@ -149,8 +164,8 @@ function getAnchorName(anchor: HTMLElement): string {
|
|
|
149
164
|
export function getFloatable(
|
|
150
165
|
anchor: HTMLElement | SVGElement,
|
|
151
166
|
content: HTMLElement | SVGElement,
|
|
152
|
-
options?:
|
|
153
|
-
):
|
|
167
|
+
options?: GetOuiFloatableOptions,
|
|
168
|
+
): OuiFloatable {
|
|
154
169
|
const {open, position} = getOptions(options);
|
|
155
170
|
|
|
156
171
|
const floatable = createFloatable(
|
|
@@ -171,7 +186,24 @@ export function getFloatable(
|
|
|
171
186
|
return floatable;
|
|
172
187
|
}
|
|
173
188
|
|
|
174
|
-
function
|
|
189
|
+
export function getOnBeforeToggleListener(element: HTMLElement): RemovableEventListener {
|
|
190
|
+
return on(
|
|
191
|
+
element,
|
|
192
|
+
EVENT_BEFORE,
|
|
193
|
+
event => {
|
|
194
|
+
const {newState, source} = event;
|
|
195
|
+
|
|
196
|
+
if (newState === STATE_OPEN && source?.getAttribute(ARIA_DISABLED) === TRUE) {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
passive: false,
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getOptions(input?: GetOuiFloatableOptions): Required<GetOuiFloatableOptions> {
|
|
175
207
|
const object = isPlainObject(input) ? input : {};
|
|
176
208
|
|
|
177
209
|
return {
|
|
@@ -180,18 +212,37 @@ function getOptions(input?: GetFloatableOptions): Required<GetFloatableOptions>
|
|
|
180
212
|
};
|
|
181
213
|
}
|
|
182
214
|
|
|
183
|
-
function getPosition(
|
|
215
|
+
function getPosition(
|
|
216
|
+
state: OuiFloatableState,
|
|
217
|
+
override?: OuiFloatablePosition,
|
|
218
|
+
): OuiFloatablePosition {
|
|
184
219
|
if (override != null && override in AREAS) {
|
|
185
220
|
return override;
|
|
186
221
|
}
|
|
187
222
|
|
|
188
|
-
|
|
189
|
-
|
|
223
|
+
const {anchor, options} = state;
|
|
224
|
+
|
|
225
|
+
if (options.attribute == null) {
|
|
226
|
+
return options.position;
|
|
190
227
|
}
|
|
191
228
|
|
|
192
|
-
const attribute =
|
|
229
|
+
const attribute = anchor.getAttribute(options.attribute) ?? '';
|
|
230
|
+
|
|
231
|
+
return attribute in AREAS ? (attribute as OuiFloatablePosition) : options.position;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getState(
|
|
235
|
+
anchor: HTMLElement,
|
|
236
|
+
content: HTMLElement,
|
|
237
|
+
options: OuiFloatableOptions,
|
|
238
|
+
): OuiFloatableState {
|
|
239
|
+
const state: OuiFloatableState = {
|
|
240
|
+
anchor,
|
|
241
|
+
content,
|
|
242
|
+
options,
|
|
243
|
+
};
|
|
193
244
|
|
|
194
|
-
return
|
|
245
|
+
return state;
|
|
195
246
|
}
|
|
196
247
|
|
|
197
248
|
export function removeFloatable(content: HTMLElement): void {
|
|
@@ -199,17 +250,21 @@ export function removeFloatable(content: HTMLElement): void {
|
|
|
199
250
|
instances.delete(content);
|
|
200
251
|
}
|
|
201
252
|
|
|
202
|
-
function setPosition(
|
|
203
|
-
const position = getPosition(
|
|
253
|
+
function setPosition(state: OuiFloatableState, override?: OuiFloatablePosition): void {
|
|
254
|
+
const position = getPosition(state, override);
|
|
204
255
|
|
|
205
256
|
const area = AREAS[position];
|
|
206
257
|
|
|
207
|
-
|
|
258
|
+
state.content.style.positionArea = area;
|
|
208
259
|
|
|
209
|
-
|
|
260
|
+
state.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
|
|
210
261
|
}
|
|
211
262
|
|
|
212
|
-
|
|
263
|
+
// #endregion
|
|
264
|
+
|
|
265
|
+
// #region Variables
|
|
266
|
+
|
|
267
|
+
const AREAS: Record<OuiFloatablePosition, string> = {
|
|
213
268
|
above: 'block-start',
|
|
214
269
|
'above-end': 'start span-start',
|
|
215
270
|
'above-start': 'start span-end',
|
|
@@ -224,14 +279,24 @@ const AREAS: Record<FloatablePosition, string> = {
|
|
|
224
279
|
'start-top': 'span-end start',
|
|
225
280
|
};
|
|
226
281
|
|
|
282
|
+
const ARIA_DISABLED = 'aria-disabled';
|
|
283
|
+
|
|
227
284
|
export const ATTRIBUTE_FLOATABLE = 'oui-floatable';
|
|
228
285
|
|
|
229
286
|
const ATTRIBUTE_POSITION = 'oui-position';
|
|
230
287
|
|
|
288
|
+
const EVENT_BEFORE = 'beforetoggle';
|
|
289
|
+
|
|
231
290
|
const POSITIONS = Object.fromEntries(
|
|
232
291
|
Object.entries(AREAS).map(([position, area]) => [area, position]),
|
|
233
|
-
) as Record<string,
|
|
292
|
+
) as Record<string, OuiFloatablePosition>;
|
|
234
293
|
|
|
235
|
-
const
|
|
294
|
+
const STATE_OPEN = 'open';
|
|
295
|
+
|
|
296
|
+
const TRUE = 'true';
|
|
297
|
+
|
|
298
|
+
const instances = new WeakMap<HTMLElement, OuiFloatable>();
|
|
236
299
|
|
|
237
300
|
let index = 0;
|
|
301
|
+
|
|
302
|
+
// #endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {getFloatable, type OuiFloatable, type OuiFloatablePosition} from './embedded';
|
package/src/js/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import './focus-trap/standalone';
|
|
2
1
|
import './movable';
|
|
3
|
-
import './popover';
|
|
4
2
|
import './sortable';
|
|
5
3
|
|
|
6
|
-
export
|
|
4
|
+
export * from './dialog';
|
|
5
|
+
export * from './floatable/standalone';
|
|
6
|
+
export * from './focus-trap/standalone';
|
|
7
|
+
export * from './popover';
|
|
7
8
|
export * from './splitter';
|
|
8
|
-
export
|
|
9
|
+
export * from './tooltip';
|