@oscarpalmer/oui 0.23.1 → 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.
Files changed (40) hide show
  1. package/dist/css/{tooltip.css → floatable.css} +59 -24
  2. package/dist/css/reset.css +17 -0
  3. package/dist/js/dialog.d.mts +38 -0
  4. package/dist/js/dialog.mjs +92 -0
  5. package/dist/js/embedded-84gxpReS.d.mts +64 -0
  6. package/dist/js/embedded-BpwzMSEF.d.mts +26 -0
  7. package/dist/js/floatable/embedded.d.mts +2 -0
  8. package/dist/js/floatable/embedded.mjs +146 -0
  9. package/dist/js/floatable/standalone.d.mts +2 -0
  10. package/dist/js/floatable/standalone.mjs +2 -0
  11. package/dist/js/focus-trap/embedded.d.mts +1 -25
  12. package/dist/js/focus-trap/embedded.mjs +1 -1
  13. package/dist/js/index.d.mts +5 -2
  14. package/dist/js/index.mjs +7 -4
  15. package/dist/js/internal/attributable.mjs +4 -3
  16. package/dist/js/internal/draggable.mjs +1 -1
  17. package/dist/js/popover.d.mts +38 -1
  18. package/dist/js/popover.mjs +81 -38
  19. package/dist/js/sortable.mjs +7 -5
  20. package/dist/js/splitter.mjs +18 -14
  21. package/dist/js/tooltip.d.mts +33 -1
  22. package/dist/js/tooltip.mjs +91 -48
  23. package/package.json +2 -2
  24. package/src/css/{tooltip.scss → floatable.scss} +47 -5
  25. package/src/css/mixins.scss +0 -2
  26. package/src/css/reset.scss +17 -0
  27. package/src/js/dialog.ts +188 -0
  28. package/src/js/floatable/embedded.ts +302 -0
  29. package/src/js/floatable/standalone.ts +1 -0
  30. package/src/js/index.ts +4 -2
  31. package/src/js/internal/attributable.ts +5 -3
  32. package/src/js/popover.ts +146 -48
  33. package/src/js/sortable.ts +8 -5
  34. package/src/js/splitter.ts +26 -9
  35. package/src/js/tooltip.ts +138 -51
  36. package/dist/css/popover.css +0 -30
  37. package/dist/js/internal/floatable.d.mts +0 -18
  38. package/dist/js/internal/floatable.mjs +0 -67
  39. package/src/css/popover.scss +0 -10
  40. package/src/js/internal/floatable.ts +0 -118
@@ -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
@@ -0,0 +1,302 @@
1
+ import {isPlainObject} from '@oscarpalmer/atoms/is';
2
+ import {on} from '@oscarpalmer/toretto/event';
3
+ import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
4
+ import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
5
+
6
+ // #region Types
7
+
8
+ export class OuiFloatable {
9
+ #destroyed = false;
10
+
11
+ readonly #state: OuiFloatableState;
12
+
13
+ /**
14
+ * Is the _Floatable_ open?
15
+ */
16
+ get open(): boolean {
17
+ return this.#state.content.checkVisibility();
18
+ }
19
+
20
+ constructor(state: OuiFloatableState, standalone: boolean) {
21
+ this.#state = state;
22
+
23
+ const {anchor, content} = state;
24
+
25
+ if (standalone) {
26
+ content.setAttribute(ATTRIBUTE_FLOATABLE, '');
27
+
28
+ content.popover = 'manual';
29
+ }
30
+
31
+ const anchorName = getAnchorName(anchor);
32
+
33
+ anchor.style.anchorName = anchorName;
34
+ content.style.positionAnchor = anchorName;
35
+ }
36
+
37
+ /**
38
+ * Destroy the _OuiFloatable_, removing it from the DOM and cleaning up references
39
+ */
40
+ destroy(): void {
41
+ if (this.#destroyed) {
42
+ return;
43
+ }
44
+
45
+ this.#destroyed = true;
46
+
47
+ removeFloatable(this.#state.content);
48
+
49
+ this.#state.anchor = null as never;
50
+ this.#state.content = null as never;
51
+ }
52
+
53
+ /**
54
+ * Closes the _OuiFloatable_
55
+ */
56
+ hide(): void {
57
+ if (!this.#destroyed && this.open) {
58
+ this.#state.content.hidePopover();
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Opens the _OuiFloatable_
64
+ */
65
+ show(): void {
66
+ if (!this.#destroyed && !this.open) {
67
+ this.#state.content.showPopover();
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Update the position of the _OuiFloatable_, with an optional new position
73
+ *
74
+ * @param position Optional new position
75
+ */
76
+ update(position?: OuiFloatablePosition): void {
77
+ if (!this.#destroyed) {
78
+ setPosition(this.#state, position);
79
+ }
80
+ }
81
+ }
82
+
83
+ export type OuiFloatableOptions = {
84
+ attribute?: string;
85
+ position: OuiFloatablePosition;
86
+ };
87
+
88
+ export type OuiFloatablePosition =
89
+ | 'above'
90
+ | 'above-end'
91
+ | 'above-start'
92
+ | 'below'
93
+ | 'below-end'
94
+ | 'below-start'
95
+ | 'end'
96
+ | 'end-bottom'
97
+ | 'end-top'
98
+ | 'start'
99
+ | 'start-bottom'
100
+ | 'start-top';
101
+
102
+ type OuiFloatableState = {
103
+ anchor: HTMLElement;
104
+ content: HTMLElement;
105
+ options: OuiFloatableOptions;
106
+ };
107
+
108
+ type GetOuiFloatableOptions = {
109
+ /**
110
+ * Open the _OuiFloatable_ when created? _(defaults to `true`)_
111
+ */
112
+ open?: boolean;
113
+ /**
114
+ * Position of the _OuiFloatable_ _(defaults to `below`)_
115
+ */
116
+ position?: OuiFloatablePosition;
117
+ };
118
+
119
+ // #endregion
120
+
121
+ // #region Functions
122
+
123
+ export function createFloatable(
124
+ anchor: HTMLElement,
125
+ content: HTMLElement,
126
+ options: OuiFloatableOptions,
127
+ standalone: boolean,
128
+ ): OuiFloatable {
129
+ if (!isHTMLOrSVGElement(anchor) || !isHTMLOrSVGElement(content)) {
130
+ throw new TypeError('Anchor and content must be an HTMLElement or SVGElement.');
131
+ }
132
+
133
+ let floatable = instances.get(content);
134
+
135
+ if (floatable == null) {
136
+ floatable = new OuiFloatable(getState(anchor, content, options), standalone);
137
+
138
+ instances.set(content, floatable);
139
+ }
140
+
141
+ return floatable;
142
+ }
143
+
144
+ function getAnchorName(anchor: HTMLElement): string {
145
+ let {anchorName} = anchor.style;
146
+
147
+ if (anchorName.length > 0) {
148
+ return anchorName;
149
+ }
150
+
151
+ index += 1;
152
+
153
+ return `--oui-floatable-anchor-${index}`;
154
+ }
155
+
156
+ /**
157
+ * Create a _Floatable_ for an anchor and content element
158
+ *
159
+ * @param anchor Anchor element
160
+ * @param content Content element
161
+ * @param options _Floatable_ options
162
+ * @returns _Floatable_ instance
163
+ */
164
+ export function getFloatable(
165
+ anchor: HTMLElement | SVGElement,
166
+ content: HTMLElement | SVGElement,
167
+ options?: GetOuiFloatableOptions,
168
+ ): OuiFloatable {
169
+ const {open, position} = getOptions(options);
170
+
171
+ const floatable = createFloatable(
172
+ anchor as HTMLElement,
173
+ content as HTMLElement,
174
+ {
175
+ position,
176
+ },
177
+ true,
178
+ );
179
+
180
+ floatable?.update(position);
181
+
182
+ if (open) {
183
+ floatable?.show();
184
+ }
185
+
186
+ return floatable;
187
+ }
188
+
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> {
207
+ const object = isPlainObject(input) ? input : {};
208
+
209
+ return {
210
+ open: typeof object.open === 'boolean' ? object.open : true,
211
+ position: object.position != null && object.position in AREAS ? object.position : 'below',
212
+ };
213
+ }
214
+
215
+ function getPosition(
216
+ state: OuiFloatableState,
217
+ override?: OuiFloatablePosition,
218
+ ): OuiFloatablePosition {
219
+ if (override != null && override in AREAS) {
220
+ return override;
221
+ }
222
+
223
+ const {anchor, options} = state;
224
+
225
+ if (options.attribute == null) {
226
+ return options.position;
227
+ }
228
+
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
+ };
244
+
245
+ return state;
246
+ }
247
+
248
+ export function removeFloatable(content: HTMLElement): void {
249
+ instances.get(content)?.destroy();
250
+ instances.delete(content);
251
+ }
252
+
253
+ function setPosition(state: OuiFloatableState, override?: OuiFloatablePosition): void {
254
+ const position = getPosition(state, override);
255
+
256
+ const area = AREAS[position];
257
+
258
+ state.content.style.positionArea = area;
259
+
260
+ state.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
261
+ }
262
+
263
+ // #endregion
264
+
265
+ // #region Variables
266
+
267
+ const AREAS: Record<OuiFloatablePosition, string> = {
268
+ above: 'block-start',
269
+ 'above-end': 'start span-start',
270
+ 'above-start': 'start span-end',
271
+ below: 'block-end',
272
+ 'below-end': 'end span-start',
273
+ 'below-start': 'end span-end',
274
+ end: 'center end',
275
+ 'end-bottom': 'span-start end',
276
+ 'end-top': 'span-end end',
277
+ start: 'center start',
278
+ 'start-bottom': 'span-start start',
279
+ 'start-top': 'span-end start',
280
+ };
281
+
282
+ const ARIA_DISABLED = 'aria-disabled';
283
+
284
+ export const ATTRIBUTE_FLOATABLE = 'oui-floatable';
285
+
286
+ const ATTRIBUTE_POSITION = 'oui-position';
287
+
288
+ const EVENT_BEFORE = 'beforetoggle';
289
+
290
+ const POSITIONS = Object.fromEntries(
291
+ Object.entries(AREAS).map(([position, area]) => [area, position]),
292
+ ) as Record<string, OuiFloatablePosition>;
293
+
294
+ const STATE_OPEN = 'open';
295
+
296
+ const TRUE = 'true';
297
+
298
+ const instances = new WeakMap<HTMLElement, OuiFloatable>();
299
+
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,7 +1,9 @@
1
- import './focus-trap/standalone';
2
1
  import './movable';
3
- import './popover';
4
2
  import './sortable';
5
3
 
4
+ export * from './dialog';
5
+ export * from './floatable/standalone';
6
+ export * from './focus-trap/standalone';
7
+ export * from './popover';
6
8
  export * from './splitter';
7
9
  export * from './tooltip';
@@ -1,3 +1,5 @@
1
+ import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
2
+
1
3
  export function attributable(
2
4
  attribute: string,
3
5
  add: (element: HTMLElement) => void,
@@ -9,7 +11,7 @@ export function attributable(
9
11
  for (let index = 0; index < length; index += 1) {
10
12
  const entry = entries[index];
11
13
 
12
- if (entry.type === 'attributes' && entry.target instanceof HTMLElement) {
14
+ if (entry.type === 'attributes' && isHTMLOrSVGElement(entry.target)) {
13
15
  if (entry.target.hasAttribute(attribute)) {
14
16
  handleAdded(attribute, [entry.target], add);
15
17
  } else {
@@ -39,7 +41,7 @@ function handleAdded(
39
41
  nodes: Node[] | NodeList,
40
42
  add: (element: HTMLElement) => void,
41
43
  ): void {
42
- const elements = [...nodes].filter(node => node instanceof HTMLElement) as HTMLElement[];
44
+ const elements = [...nodes].filter(isHTMLOrSVGElement) as HTMLElement[];
43
45
 
44
46
  const {length} = elements;
45
47
 
@@ -57,7 +59,7 @@ function handleAdded(
57
59
  }
58
60
 
59
61
  function handleRemoved(nodes: Node[] | NodeList, remove: (element: HTMLElement) => void): void {
60
- const elements = [...nodes].filter(node => node instanceof HTMLElement) as HTMLElement[];
62
+ const elements = [...nodes].filter(isHTMLOrSVGElement) as HTMLElement[];
61
63
 
62
64
  const {length} = elements;
63
65