@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.
@@ -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
- export class Floatable {
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
- public anchor: HTMLElement,
16
- public content: HTMLElement,
17
- public options: FloatableOptions,
18
- standalone: boolean,
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 floatable, removing it from the DOM and cleaning up references
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
- * Hide the floatable
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
- * Show the floatable
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 floatable, with an optional new position
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?: FloatablePosition): void {
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 FloatableOptions = {
83
+ export type OuiFloatableOptions = {
79
84
  attribute?: string;
80
- position: FloatablePosition;
85
+ position: OuiFloatablePosition;
81
86
  };
82
87
 
83
- export type FloatablePosition =
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 GetFloatableOptions = {
102
+ type OuiFloatableState = {
103
+ anchor: HTMLElement;
104
+ content: HTMLElement;
105
+ options: OuiFloatableOptions;
106
+ };
107
+
108
+ type GetOuiFloatableOptions = {
98
109
  /**
99
- * Open the _Floatable_ when created? _(defaults to `true`)_
110
+ * Open the _OuiFloatable_ when created? _(defaults to `true`)_
100
111
  */
101
112
  open?: boolean;
102
113
  /**
103
- * Position of the _Floatable_ _(defaults to `below`)_
114
+ * Position of the _OuiFloatable_ _(defaults to `below`)_
104
115
  */
105
- position?: FloatablePosition;
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: FloatableOptions,
126
+ options: OuiFloatableOptions,
112
127
  standalone: boolean,
113
- ): Floatable {
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 Floatable(anchor, content, options, standalone);
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?: GetFloatableOptions,
153
- ): Floatable {
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 getOptions(input?: GetFloatableOptions): Required<GetFloatableOptions> {
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(floatable: Floatable, override?: FloatablePosition): FloatablePosition {
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
- if (floatable.options.attribute == null) {
189
- return floatable.options.position;
223
+ const {anchor, options} = state;
224
+
225
+ if (options.attribute == null) {
226
+ return options.position;
190
227
  }
191
228
 
192
- const attribute = floatable.anchor.getAttribute(floatable.options.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 attribute in AREAS ? (attribute as FloatablePosition) : floatable.options.position;
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(floatable: Floatable, override?: FloatablePosition): void {
203
- const position = getPosition(floatable, override);
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
- floatable.content.style.positionArea = area;
258
+ state.content.style.positionArea = area;
208
259
 
209
- floatable.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
260
+ state.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
210
261
  }
211
262
 
212
- const AREAS: Record<FloatablePosition, string> = {
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, FloatablePosition>;
292
+ ) as Record<string, OuiFloatablePosition>;
234
293
 
235
- const instances = new WeakMap<HTMLElement, Floatable>();
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 {getFloatable} from './floatable'
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 {setTooltipDelay} from './tooltip';
9
+ export * from './tooltip';