@unofficialbox/box-open-elements 0.2.0 → 0.3.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/README.md +1 -0
- package/dist/components/actions/button.d.ts +15 -0
- package/dist/components/actions/button.js +107 -3
- package/dist/components/actions/menu.d.ts +10 -0
- package/dist/components/actions/menu.js +68 -11
- package/dist/components/collections/table.d.ts +53 -0
- package/dist/components/collections/table.js +323 -0
- package/dist/components/collections/thumbnail-card.d.ts +19 -0
- package/dist/components/collections/thumbnail-card.js +210 -0
- package/dist/components/feedback/badgeable.d.ts +14 -0
- package/dist/components/feedback/badgeable.js +84 -0
- package/dist/components/forms/date-field.d.ts +1 -0
- package/dist/components/forms/date-field.js +7 -2
- package/dist/components/forms/pill-selector-dropdown.d.ts +11 -0
- package/dist/components/forms/pill-selector-dropdown.js +127 -0
- package/dist/components/forms/search-field.d.ts +1 -0
- package/dist/components/forms/search-field.js +7 -2
- package/dist/components/forms/select.d.ts +1 -0
- package/dist/components/forms/select.js +7 -2
- package/dist/components/forms/text-field.d.ts +1 -0
- package/dist/components/forms/text-field.js +8 -2
- package/dist/components/navigation/breadcrumb.d.ts +30 -0
- package/dist/components/navigation/breadcrumb.js +190 -0
- package/dist/components/navigation/tabs.d.ts +4 -0
- package/dist/components/navigation/tabs.js +53 -5
- package/dist/components/overlays/context-menu.d.ts +42 -0
- package/dist/components/overlays/context-menu.js +260 -0
- package/dist/components/overlays/dialog.d.ts +8 -0
- package/dist/components/overlays/dialog.js +45 -2
- package/dist/components/overlays/popover.d.ts +9 -1
- package/dist/components/overlays/popover.js +37 -22
- package/dist/core/form-associated.d.ts +32 -0
- package/dist/core/form-associated.js +114 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/foundations/overlay/index.d.ts +2 -0
- package/dist/foundations/overlay/index.js +1 -0
- package/dist/foundations/overlay/positioning.d.ts +74 -0
- package/dist/foundations/overlay/positioning.js +158 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/patterns/content-explorer/adapters/table.js +8 -1
- package/dist/patterns/search/filter-bar.js +12 -6
- package/package.json +1 -1
|
@@ -29,10 +29,26 @@ export declare const rangeFromFormValue: (value: FormValue, name: string, fallba
|
|
|
29
29
|
export declare const boeFormFieldErrorStyles = "\n :host([invalid]) [part=\"input\"],\n :host([invalid]) [part=\"textarea\"],\n :host([invalid]) [part=\"select\"],\n :host([invalid]) [part=\"control\"],\n :host([invalid]) [part=\"editor\"] {\n border-color: var(--boe-token-surface-status-surface-error, #ed3757);\n }\n\n :host([invalid]) [part=\"input\"]:focus-visible,\n :host([invalid]) [part=\"textarea\"]:focus-visible,\n :host([invalid]) [part=\"select\"]:focus-visible,\n :host([invalid]) [part=\"control\"]:focus-visible {\n border-color: var(--boe-token-surface-status-surface-error, #ed3757);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 22%, transparent);\n }\n\n [part=\"error-message\"] {\n margin: 0;\n color: var(--boe-token-surface-status-surface-error, #ed3757);\n font-size: 0.82rem;\n line-height: 1.35;\n }\n\n [part=\"error-message\"][hidden] {\n display: none;\n }\n";
|
|
30
30
|
export declare const FORM_ERROR_MESSAGE_ID = "boe-field-error";
|
|
31
31
|
export declare const formErrorMessageMarkup: () => string;
|
|
32
|
+
export declare const FORM_DESCRIPTION_ID = "boe-field-description";
|
|
33
|
+
/** Optional help text rendered under the label (shown only when `description` is set). */
|
|
34
|
+
export declare const formDescriptionMarkup: () => string;
|
|
35
|
+
/**
|
|
36
|
+
* Shared chrome for the field-feature set: help-text `description`, the
|
|
37
|
+
* `required` indicator, and the visually-hidden `hide-label` treatment.
|
|
38
|
+
* Include alongside `boeFormFieldErrorStyles` in a field's stylesheet.
|
|
39
|
+
*/
|
|
40
|
+
export declare const boeFormFieldSupportStyles = "\n [part=\"description\"] {\n margin: 0;\n color: var(--boe-token-text-text-secondary, #6f6f6f);\n font-size: 0.82rem;\n line-height: 1.35;\n }\n\n [part=\"description\"][hidden] {\n display: none;\n }\n\n [part=\"label\"] .boe-required-mark {\n margin-inline-start: 2px;\n color: var(--boe-token-surface-status-surface-error, #ed3757);\n }\n\n :host([hide-label]) [part=\"label\"] {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n";
|
|
32
41
|
export declare abstract class FormAssociatedElement extends BaseElement {
|
|
33
42
|
static formAssociated: boolean;
|
|
34
43
|
/** Attributes every form control should observe (merge into subclass lists). */
|
|
35
44
|
static readonly formObservedAttributes: readonly ["name", "invalid", "error-message"];
|
|
45
|
+
/**
|
|
46
|
+
* `formObservedAttributes` plus the shared field-feature attributes
|
|
47
|
+
* (`required`, `description`, `hide-label`). A field that renders a label +
|
|
48
|
+
* `formDescriptionMarkup()` and calls `applyFieldSupport()` should observe
|
|
49
|
+
* these instead.
|
|
50
|
+
*/
|
|
51
|
+
static readonly fieldObservedAttributes: readonly ["name", "invalid", "error-message", "required", "description", "hide-label"];
|
|
36
52
|
/** ElementInternals for form association (public for tests / advanced hosts). */
|
|
37
53
|
readonly internals: ElementInternals;
|
|
38
54
|
private defaultFormValue;
|
|
@@ -44,6 +60,14 @@ export declare abstract class FormAssociatedElement extends BaseElement {
|
|
|
44
60
|
set invalid(value: boolean);
|
|
45
61
|
get errorMessage(): string;
|
|
46
62
|
set errorMessage(value: string);
|
|
63
|
+
get required(): boolean;
|
|
64
|
+
set required(value: boolean);
|
|
65
|
+
/** Help text rendered under the label (empty string hides it). */
|
|
66
|
+
get description(): string;
|
|
67
|
+
set description(value: string);
|
|
68
|
+
/** Keep the label for assistive tech but hide it visually. */
|
|
69
|
+
get hideLabel(): boolean;
|
|
70
|
+
set hideLabel(value: boolean);
|
|
47
71
|
/** Value reported to the owning form (`null` omits the control). */
|
|
48
72
|
protected abstract getFormValue(): FormValue;
|
|
49
73
|
/** Restore a value from form reset / autocomplete. */
|
|
@@ -60,4 +84,12 @@ export declare abstract class FormAssociatedElement extends BaseElement {
|
|
|
60
84
|
protected applyInvalidState(control: HTMLElement | null | undefined, messageEl: HTMLElement | null | undefined): void;
|
|
61
85
|
/** Apply invalid ARIA to every focusable control in a multi-option field. */
|
|
62
86
|
protected applyInvalidStateToControls(controls: Iterable<HTMLElement | null | undefined>, messageEl: HTMLElement | null | undefined): void;
|
|
87
|
+
/**
|
|
88
|
+
* Apply the shared field features to a rendered field: the `required`
|
|
89
|
+
* indicator + `aria-required` on the control, the `description` help text +
|
|
90
|
+
* `aria-describedby`, and the `hide-label` visually-hidden treatment (handled
|
|
91
|
+
* in CSS via `:host([hide-label])`). Call after setting the label's text
|
|
92
|
+
* content each update, since setting `textContent` clears the appended mark.
|
|
93
|
+
*/
|
|
94
|
+
protected applyFieldSupport(labelEl: HTMLElement | null | undefined, control: HTMLElement | null | undefined, descriptionEl: HTMLElement | null | undefined): void;
|
|
63
95
|
}
|
|
@@ -124,10 +124,61 @@ export const boeFormFieldErrorStyles = `
|
|
|
124
124
|
`;
|
|
125
125
|
export const FORM_ERROR_MESSAGE_ID = "boe-field-error";
|
|
126
126
|
export const formErrorMessageMarkup = () => `<p part="error-message" id="${FORM_ERROR_MESSAGE_ID}" role="alert" hidden></p>`;
|
|
127
|
+
export const FORM_DESCRIPTION_ID = "boe-field-description";
|
|
128
|
+
/** Optional help text rendered under the label (shown only when `description` is set). */
|
|
129
|
+
export const formDescriptionMarkup = () => `<p part="description" id="${FORM_DESCRIPTION_ID}" hidden></p>`;
|
|
130
|
+
/**
|
|
131
|
+
* Shared chrome for the field-feature set: help-text `description`, the
|
|
132
|
+
* `required` indicator, and the visually-hidden `hide-label` treatment.
|
|
133
|
+
* Include alongside `boeFormFieldErrorStyles` in a field's stylesheet.
|
|
134
|
+
*/
|
|
135
|
+
export const boeFormFieldSupportStyles = `
|
|
136
|
+
[part="description"] {
|
|
137
|
+
margin: 0;
|
|
138
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
139
|
+
font-size: 0.82rem;
|
|
140
|
+
line-height: 1.35;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
[part="description"][hidden] {
|
|
144
|
+
display: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
[part="label"] .boe-required-mark {
|
|
148
|
+
margin-inline-start: 2px;
|
|
149
|
+
color: var(--boe-token-surface-status-surface-error, #ed3757);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
:host([hide-label]) [part="label"] {
|
|
153
|
+
position: absolute;
|
|
154
|
+
width: 1px;
|
|
155
|
+
height: 1px;
|
|
156
|
+
padding: 0;
|
|
157
|
+
margin: -1px;
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
clip: rect(0, 0, 0, 0);
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
border: 0;
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
127
164
|
export class FormAssociatedElement extends BaseElement {
|
|
128
165
|
static formAssociated = true;
|
|
129
166
|
/** Attributes every form control should observe (merge into subclass lists). */
|
|
130
167
|
static formObservedAttributes = ["name", "invalid", "error-message"];
|
|
168
|
+
/**
|
|
169
|
+
* `formObservedAttributes` plus the shared field-feature attributes
|
|
170
|
+
* (`required`, `description`, `hide-label`). A field that renders a label +
|
|
171
|
+
* `formDescriptionMarkup()` and calls `applyFieldSupport()` should observe
|
|
172
|
+
* these instead.
|
|
173
|
+
*/
|
|
174
|
+
static fieldObservedAttributes = [
|
|
175
|
+
"name",
|
|
176
|
+
"invalid",
|
|
177
|
+
"error-message",
|
|
178
|
+
"required",
|
|
179
|
+
"description",
|
|
180
|
+
"hide-label",
|
|
181
|
+
];
|
|
131
182
|
/** ElementInternals for form association (public for tests / advanced hosts). */
|
|
132
183
|
internals;
|
|
133
184
|
defaultFormValue = null;
|
|
@@ -164,6 +215,31 @@ export class FormAssociatedElement extends BaseElement {
|
|
|
164
215
|
this.removeAttribute("error-message");
|
|
165
216
|
}
|
|
166
217
|
}
|
|
218
|
+
get required() {
|
|
219
|
+
return this.hasAttribute("required");
|
|
220
|
+
}
|
|
221
|
+
set required(value) {
|
|
222
|
+
this.toggleAttribute("required", Boolean(value));
|
|
223
|
+
}
|
|
224
|
+
/** Help text rendered under the label (empty string hides it). */
|
|
225
|
+
get description() {
|
|
226
|
+
return this.getAttribute("description") ?? "";
|
|
227
|
+
}
|
|
228
|
+
set description(value) {
|
|
229
|
+
if (value) {
|
|
230
|
+
this.setAttribute("description", value);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
this.removeAttribute("description");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Keep the label for assistive tech but hide it visually. */
|
|
237
|
+
get hideLabel() {
|
|
238
|
+
return this.hasAttribute("hide-label");
|
|
239
|
+
}
|
|
240
|
+
set hideLabel(value) {
|
|
241
|
+
this.toggleAttribute("hide-label", Boolean(value));
|
|
242
|
+
}
|
|
167
243
|
connectedCallback() {
|
|
168
244
|
super.connectedCallback();
|
|
169
245
|
if (!this.defaultCaptured) {
|
|
@@ -215,4 +291,42 @@ export class FormAssociatedElement extends BaseElement {
|
|
|
215
291
|
}
|
|
216
292
|
this.syncFormAssociation();
|
|
217
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Apply the shared field features to a rendered field: the `required`
|
|
296
|
+
* indicator + `aria-required` on the control, the `description` help text +
|
|
297
|
+
* `aria-describedby`, and the `hide-label` visually-hidden treatment (handled
|
|
298
|
+
* in CSS via `:host([hide-label])`). Call after setting the label's text
|
|
299
|
+
* content each update, since setting `textContent` clears the appended mark.
|
|
300
|
+
*/
|
|
301
|
+
applyFieldSupport(labelEl, control, descriptionEl) {
|
|
302
|
+
const required = this.required;
|
|
303
|
+
const description = this.description;
|
|
304
|
+
if (labelEl) {
|
|
305
|
+
const existing = labelEl.querySelector(".boe-required-mark");
|
|
306
|
+
if (required && !existing) {
|
|
307
|
+
const mark = document.createElement("span");
|
|
308
|
+
mark.className = "boe-required-mark";
|
|
309
|
+
mark.setAttribute("aria-hidden", "true");
|
|
310
|
+
mark.textContent = "*";
|
|
311
|
+
labelEl.append(mark);
|
|
312
|
+
}
|
|
313
|
+
else if (!required && existing) {
|
|
314
|
+
existing.remove();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (control) {
|
|
318
|
+
control.toggleAttribute("required", required);
|
|
319
|
+
control.setAttribute("aria-required", String(required));
|
|
320
|
+
if (description) {
|
|
321
|
+
control.setAttribute("aria-describedby", FORM_DESCRIPTION_ID);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
control.removeAttribute("aria-describedby");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (descriptionEl) {
|
|
328
|
+
descriptionEl.textContent = description;
|
|
329
|
+
descriptionEl.hidden = description.length === 0;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
218
332
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export { Controller } from "./controller.js";
|
|
|
2
2
|
export { TypedEventEmitter } from "./event-emitter.js";
|
|
3
3
|
export type { EventMap, Unsubscribe } from "./event-emitter.js";
|
|
4
4
|
export { BaseElement } from "./element.js";
|
|
5
|
-
export { FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, formDataFromNamedValues, formDataFromRange, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
|
|
5
|
+
export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
|
|
6
6
|
export type { FormValue } from "./form-associated.js";
|
package/dist/core/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { Controller } from "./controller.js";
|
|
2
2
|
export { TypedEventEmitter } from "./event-emitter.js";
|
|
3
3
|
export { BaseElement } from "./element.js";
|
|
4
|
-
export { FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, formDataFromNamedValues, formDataFromRange, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
|
|
4
|
+
export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./positioning.js";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport-aware anchored positioning for overlays (popover, tooltip, dropdown,
|
|
3
|
+
* menu, context-menu). box-open-elements has no portal, so floating surfaces
|
|
4
|
+
* position with `position: fixed` in viewport coordinates — which also lets them
|
|
5
|
+
* escape `overflow: hidden` / stacking-context ancestors, the job a portal does
|
|
6
|
+
* in box-ui-elements.
|
|
7
|
+
*
|
|
8
|
+
* The geometry core (`resolvePosition`) is pure and DOM-free so it can be unit
|
|
9
|
+
* tested with plain rects; the DOM helpers (`anchorFloating`, `trackAnchor`)
|
|
10
|
+
* read live rects and apply styles.
|
|
11
|
+
*
|
|
12
|
+
* Behaviour mirrors box-ui-elements' flyout: a preferred placement, `flip` to
|
|
13
|
+
* the opposite side when it would overflow, and `shift` along the cross axis to
|
|
14
|
+
* stay within the viewport.
|
|
15
|
+
*/
|
|
16
|
+
/** Primary side the floating element sits on, relative to the anchor. */
|
|
17
|
+
export type OverlaySide = "top" | "bottom" | "left" | "right";
|
|
18
|
+
/** Cross-axis alignment of the floating element against the anchor edge. */
|
|
19
|
+
export type OverlayAlign = "start" | "center" | "end";
|
|
20
|
+
export interface OverlayPlacement {
|
|
21
|
+
side: OverlaySide;
|
|
22
|
+
align: OverlayAlign;
|
|
23
|
+
}
|
|
24
|
+
export interface Rect {
|
|
25
|
+
top: number;
|
|
26
|
+
left: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
}
|
|
30
|
+
export interface Size {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
}
|
|
34
|
+
export interface PositionOptions {
|
|
35
|
+
/** Preferred placement. Default `{ side: "bottom", align: "start" }`. */
|
|
36
|
+
placement?: OverlayPlacement;
|
|
37
|
+
/** Gap between the anchor edge and the floating element, in px. Default 4. */
|
|
38
|
+
offset?: number;
|
|
39
|
+
/** Flip to the opposite side when the preferred side would overflow. Default true. */
|
|
40
|
+
flip?: boolean;
|
|
41
|
+
/** Shift along the cross axis to keep the element inside the viewport. Default true. */
|
|
42
|
+
shift?: boolean;
|
|
43
|
+
/** Minimum gap kept from each viewport edge, in px. Default 8. */
|
|
44
|
+
padding?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface PositionResult {
|
|
47
|
+
/** Viewport x (use as `left` for a `position: fixed` element). */
|
|
48
|
+
x: number;
|
|
49
|
+
/** Viewport y (use as `top`). */
|
|
50
|
+
y: number;
|
|
51
|
+
/** The side actually used (may differ from the request after a flip). */
|
|
52
|
+
side: OverlaySide;
|
|
53
|
+
/** The cross-axis alignment used. */
|
|
54
|
+
align: OverlayAlign;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compute the viewport position of a floating element against an anchor. Pure —
|
|
58
|
+
* pass plain rects (e.g. from `getBoundingClientRect()`) and the viewport size.
|
|
59
|
+
*/
|
|
60
|
+
export declare const resolvePosition: (anchor: Rect, floating: Size, viewport: Size, options?: PositionOptions) => PositionResult;
|
|
61
|
+
/** Parse a placement string like `"bottom-start"` / `"top"` / `"right-center"`. */
|
|
62
|
+
export declare const parsePlacement: (value: string | null | undefined) => OverlayPlacement | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Position `floating` against `anchor` once, applying `position: fixed` and the
|
|
65
|
+
* resolved coordinates. Returns the resolved placement (e.g. for arrow styling).
|
|
66
|
+
*/
|
|
67
|
+
export declare const anchorFloating: (anchor: HTMLElement, floating: HTMLElement, options?: PositionOptions) => PositionResult;
|
|
68
|
+
/**
|
|
69
|
+
* Keep `floating` positioned against `anchor` while it is open: reposition on
|
|
70
|
+
* scroll (capture, to catch scrolling ancestors) and resize. Returns a cleanup
|
|
71
|
+
* that removes the listeners. Call `anchorFloating` once up front is handled
|
|
72
|
+
* internally.
|
|
73
|
+
*/
|
|
74
|
+
export declare const trackAnchor: (anchor: HTMLElement, floating: HTMLElement, options?: PositionOptions, onReposition?: (result: PositionResult) => void) => (() => void);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport-aware anchored positioning for overlays (popover, tooltip, dropdown,
|
|
3
|
+
* menu, context-menu). box-open-elements has no portal, so floating surfaces
|
|
4
|
+
* position with `position: fixed` in viewport coordinates — which also lets them
|
|
5
|
+
* escape `overflow: hidden` / stacking-context ancestors, the job a portal does
|
|
6
|
+
* in box-ui-elements.
|
|
7
|
+
*
|
|
8
|
+
* The geometry core (`resolvePosition`) is pure and DOM-free so it can be unit
|
|
9
|
+
* tested with plain rects; the DOM helpers (`anchorFloating`, `trackAnchor`)
|
|
10
|
+
* read live rects and apply styles.
|
|
11
|
+
*
|
|
12
|
+
* Behaviour mirrors box-ui-elements' flyout: a preferred placement, `flip` to
|
|
13
|
+
* the opposite side when it would overflow, and `shift` along the cross axis to
|
|
14
|
+
* stay within the viewport.
|
|
15
|
+
*/
|
|
16
|
+
const OPPOSITE = {
|
|
17
|
+
top: "bottom",
|
|
18
|
+
bottom: "top",
|
|
19
|
+
left: "right",
|
|
20
|
+
right: "left",
|
|
21
|
+
};
|
|
22
|
+
const isVertical = (side) => side === "top" || side === "bottom";
|
|
23
|
+
/** Cross-axis start coordinate for an alignment. */
|
|
24
|
+
const alignStart = (anchorStart, anchorSize, floatingSize, align) => {
|
|
25
|
+
if (align === "center")
|
|
26
|
+
return anchorStart + anchorSize / 2 - floatingSize / 2;
|
|
27
|
+
if (align === "end")
|
|
28
|
+
return anchorStart + anchorSize - floatingSize;
|
|
29
|
+
return anchorStart;
|
|
30
|
+
};
|
|
31
|
+
/** Main-axis coordinate for a side (top/left of the floating element). */
|
|
32
|
+
const sideCoord = (anchor, floating, side, offset) => {
|
|
33
|
+
switch (side) {
|
|
34
|
+
case "bottom":
|
|
35
|
+
return anchor.top + anchor.height + offset;
|
|
36
|
+
case "top":
|
|
37
|
+
return anchor.top - floating.height - offset;
|
|
38
|
+
case "right":
|
|
39
|
+
return anchor.left + anchor.width + offset;
|
|
40
|
+
case "left":
|
|
41
|
+
return anchor.left - floating.width - offset;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/** Space available between the anchor and the viewport edge on a given side. */
|
|
45
|
+
const spaceOn = (anchor, viewport, side) => {
|
|
46
|
+
switch (side) {
|
|
47
|
+
case "top":
|
|
48
|
+
return anchor.top;
|
|
49
|
+
case "bottom":
|
|
50
|
+
return viewport.height - (anchor.top + anchor.height);
|
|
51
|
+
case "left":
|
|
52
|
+
return anchor.left;
|
|
53
|
+
case "right":
|
|
54
|
+
return viewport.width - (anchor.left + anchor.width);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const clamp = (value, min, max) => max < min ? min : Math.min(Math.max(value, min), max);
|
|
58
|
+
/**
|
|
59
|
+
* Compute the viewport position of a floating element against an anchor. Pure —
|
|
60
|
+
* pass plain rects (e.g. from `getBoundingClientRect()`) and the viewport size.
|
|
61
|
+
*/
|
|
62
|
+
export const resolvePosition = (anchor, floating, viewport, options = {}) => {
|
|
63
|
+
const { side: wantSide, align } = options.placement ?? { side: "bottom", align: "start" };
|
|
64
|
+
const offset = options.offset ?? 4;
|
|
65
|
+
const flip = options.flip ?? true;
|
|
66
|
+
const shift = options.shift ?? true;
|
|
67
|
+
const padding = options.padding ?? 8;
|
|
68
|
+
// Flip: if the preferred side lacks room for the element (+offset) and the
|
|
69
|
+
// opposite side has more, use the opposite side.
|
|
70
|
+
let side = wantSide;
|
|
71
|
+
if (flip) {
|
|
72
|
+
const need = (isVertical(wantSide) ? floating.height : floating.width) + offset;
|
|
73
|
+
const room = spaceOn(anchor, viewport, wantSide);
|
|
74
|
+
if (room < need) {
|
|
75
|
+
const opposite = OPPOSITE[wantSide];
|
|
76
|
+
if (spaceOn(anchor, viewport, opposite) > room)
|
|
77
|
+
side = opposite;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let x;
|
|
81
|
+
let y;
|
|
82
|
+
if (isVertical(side)) {
|
|
83
|
+
y = sideCoord(anchor, floating, side, offset);
|
|
84
|
+
x = alignStart(anchor.left, anchor.width, floating.width, align);
|
|
85
|
+
if (shift)
|
|
86
|
+
x = clamp(x, padding, viewport.width - floating.width - padding);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
x = sideCoord(anchor, floating, side, offset);
|
|
90
|
+
y = alignStart(anchor.top, anchor.height, floating.height, align);
|
|
91
|
+
if (shift)
|
|
92
|
+
y = clamp(y, padding, viewport.height - floating.height - padding);
|
|
93
|
+
}
|
|
94
|
+
return { x, y, side, align };
|
|
95
|
+
};
|
|
96
|
+
/** Parse a placement string like `"bottom-start"` / `"top"` / `"right-center"`. */
|
|
97
|
+
export const parsePlacement = (value) => {
|
|
98
|
+
if (!value)
|
|
99
|
+
return undefined;
|
|
100
|
+
const [rawSide, rawAlign] = value.trim().toLowerCase().split("-");
|
|
101
|
+
const sideMap = {
|
|
102
|
+
top: "top",
|
|
103
|
+
bottom: "bottom",
|
|
104
|
+
left: "left",
|
|
105
|
+
right: "right",
|
|
106
|
+
// Legacy popover names (start = inline-start = left, end = right).
|
|
107
|
+
start: "left",
|
|
108
|
+
end: "right",
|
|
109
|
+
};
|
|
110
|
+
const side = sideMap[rawSide];
|
|
111
|
+
if (!side)
|
|
112
|
+
return undefined;
|
|
113
|
+
const alignMap = {
|
|
114
|
+
start: "start",
|
|
115
|
+
center: "center",
|
|
116
|
+
middle: "center",
|
|
117
|
+
end: "end",
|
|
118
|
+
};
|
|
119
|
+
return { side, align: alignMap[rawAlign] ?? "start" };
|
|
120
|
+
};
|
|
121
|
+
const viewportSize = () => ({
|
|
122
|
+
width: window.innerWidth,
|
|
123
|
+
height: window.innerHeight,
|
|
124
|
+
});
|
|
125
|
+
/**
|
|
126
|
+
* Position `floating` against `anchor` once, applying `position: fixed` and the
|
|
127
|
+
* resolved coordinates. Returns the resolved placement (e.g. for arrow styling).
|
|
128
|
+
*/
|
|
129
|
+
export const anchorFloating = (anchor, floating, options = {}) => {
|
|
130
|
+
const a = anchor.getBoundingClientRect();
|
|
131
|
+
const f = floating.getBoundingClientRect();
|
|
132
|
+
const result = resolvePosition({ top: a.top, left: a.left, width: a.width, height: a.height }, { width: f.width, height: f.height }, viewportSize(), options);
|
|
133
|
+
floating.style.position = "fixed";
|
|
134
|
+
floating.style.left = `${Math.round(result.x)}px`;
|
|
135
|
+
floating.style.top = `${Math.round(result.y)}px`;
|
|
136
|
+
floating.style.margin = "0";
|
|
137
|
+
return result;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Keep `floating` positioned against `anchor` while it is open: reposition on
|
|
141
|
+
* scroll (capture, to catch scrolling ancestors) and resize. Returns a cleanup
|
|
142
|
+
* that removes the listeners. Call `anchorFloating` once up front is handled
|
|
143
|
+
* internally.
|
|
144
|
+
*/
|
|
145
|
+
export const trackAnchor = (anchor, floating, options = {}, onReposition) => {
|
|
146
|
+
const reposition = () => {
|
|
147
|
+
if (!anchor.isConnected || !floating.isConnected)
|
|
148
|
+
return;
|
|
149
|
+
onReposition?.(anchorFloating(anchor, floating, options));
|
|
150
|
+
};
|
|
151
|
+
reposition();
|
|
152
|
+
window.addEventListener("scroll", reposition, true);
|
|
153
|
+
window.addEventListener("resize", reposition);
|
|
154
|
+
return () => {
|
|
155
|
+
window.removeEventListener("scroll", reposition, true);
|
|
156
|
+
window.removeEventListener("resize", reposition);
|
|
157
|
+
};
|
|
158
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type { JsonSchema } from "./core/json-schema.js";
|
|
|
4
4
|
export { DESIGN_SYSTEM_CHANGE_EVENT, applyDesignTokens, boxDarkDesignSystem, boxDefaultDesignSystem, createDesignTokenStyleText, getActiveDesignSystem, getDesignSystem, listDesignSystems, registerBoxDarkDesignSystem, registerBoxDefaultDesignSystem, registerDesignSystem, resolveDesignIcon, resolveDesignIllustration, resolveDesignSystemTokens, setActiveDesignSystem, } from "./foundations/tokens/index.js";
|
|
5
5
|
export type { ApplyDesignTokensOptions, DesignAssetRenderContext, DesignAssetRenderer, DesignSystemDefinition, DesignTokenMap, } from "./foundations/tokens/index.js";
|
|
6
6
|
export { boxIconography, boxIconographyAliases } from "./foundations/icons/index.js";
|
|
7
|
+
export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./foundations/overlay/index.js";
|
|
8
|
+
export type { OverlayAlign, OverlayPlacement, OverlaySide, PositionOptions, PositionResult, } from "./foundations/overlay/index.js";
|
|
7
9
|
export * from "./components/actions/button.js";
|
|
8
10
|
export * from "./components/actions/button-group.js";
|
|
9
11
|
export * from "./components/actions/icon-button.js";
|
|
@@ -17,10 +19,13 @@ export * from "./components/collections/datalist-item.js";
|
|
|
17
19
|
export * from "./components/collections/draggable-list.js";
|
|
18
20
|
export * from "./components/collections/grid-view.js";
|
|
19
21
|
export * from "./components/collections/pagination.js";
|
|
22
|
+
export * from "./components/collections/table.js";
|
|
23
|
+
export * from "./components/collections/thumbnail-card.js";
|
|
20
24
|
export * from "./components/collections/tree.js";
|
|
21
25
|
export * from "./components/collections/tree-grid.js";
|
|
22
26
|
export * from "./components/feedback/alert.js";
|
|
23
27
|
export * from "./components/feedback/badge.js";
|
|
28
|
+
export * from "./components/feedback/badgeable.js";
|
|
24
29
|
export * from "./components/feedback/chip.js";
|
|
25
30
|
export * from "./components/feedback/empty-state.js";
|
|
26
31
|
export * from "./components/feedback/error-mask.js";
|
|
@@ -36,6 +41,7 @@ export * from "./components/files/drop-zone.js";
|
|
|
36
41
|
export * from "./components/identity/avatar.js";
|
|
37
42
|
export * from "./components/identity/persona.js";
|
|
38
43
|
export * from "./components/navigation/accordion.js";
|
|
44
|
+
export * from "./components/navigation/breadcrumb.js";
|
|
39
45
|
export * from "./components/navigation/tabs.js";
|
|
40
46
|
export * from "./components/visuals/illustration.js";
|
|
41
47
|
export * from "./components/forms/checkbox.js";
|
|
@@ -71,6 +77,7 @@ export * from "./components/layout/nav-sidebar.js";
|
|
|
71
77
|
export * from "./components/layout/section.js";
|
|
72
78
|
export * from "./components/layout/sidebar-toggle-button.js";
|
|
73
79
|
export * from "./components/layout/split-view.js";
|
|
80
|
+
export * from "./components/overlays/context-menu.js";
|
|
74
81
|
export * from "./components/overlays/dialog.js";
|
|
75
82
|
export * from "./components/overlays/drawer.js";
|
|
76
83
|
export * from "./components/overlays/popover.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export { Controller, TypedEventEmitter } from "./core/index.js";
|
|
|
3
3
|
// Foundations
|
|
4
4
|
export { DESIGN_SYSTEM_CHANGE_EVENT, applyDesignTokens, boxDarkDesignSystem, boxDefaultDesignSystem, createDesignTokenStyleText, getActiveDesignSystem, getDesignSystem, listDesignSystems, registerBoxDarkDesignSystem, registerBoxDefaultDesignSystem, registerDesignSystem, resolveDesignIcon, resolveDesignIllustration, resolveDesignSystemTokens, setActiveDesignSystem, } from "./foundations/tokens/index.js";
|
|
5
5
|
export { boxIconography, boxIconographyAliases } from "./foundations/icons/index.js";
|
|
6
|
+
// Foundations: viewport-aware overlay positioning (for building custom overlays)
|
|
7
|
+
export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./foundations/overlay/index.js";
|
|
6
8
|
// Components: actions
|
|
7
9
|
export * from "./components/actions/button.js";
|
|
8
10
|
export * from "./components/actions/button-group.js";
|
|
@@ -18,11 +20,14 @@ export * from "./components/collections/datalist-item.js";
|
|
|
18
20
|
export * from "./components/collections/draggable-list.js";
|
|
19
21
|
export * from "./components/collections/grid-view.js";
|
|
20
22
|
export * from "./components/collections/pagination.js";
|
|
23
|
+
export * from "./components/collections/table.js";
|
|
24
|
+
export * from "./components/collections/thumbnail-card.js";
|
|
21
25
|
export * from "./components/collections/tree.js";
|
|
22
26
|
export * from "./components/collections/tree-grid.js";
|
|
23
27
|
// Components: feedback
|
|
24
28
|
export * from "./components/feedback/alert.js";
|
|
25
29
|
export * from "./components/feedback/badge.js";
|
|
30
|
+
export * from "./components/feedback/badgeable.js";
|
|
26
31
|
export * from "./components/feedback/chip.js";
|
|
27
32
|
export * from "./components/feedback/empty-state.js";
|
|
28
33
|
export * from "./components/feedback/error-mask.js";
|
|
@@ -41,6 +46,7 @@ export * from "./components/identity/avatar.js";
|
|
|
41
46
|
export * from "./components/identity/persona.js";
|
|
42
47
|
// Components: navigation
|
|
43
48
|
export * from "./components/navigation/accordion.js";
|
|
49
|
+
export * from "./components/navigation/breadcrumb.js";
|
|
44
50
|
export * from "./components/navigation/tabs.js";
|
|
45
51
|
// Components: visuals
|
|
46
52
|
export * from "./components/visuals/illustration.js";
|
|
@@ -80,6 +86,7 @@ export * from "./components/layout/section.js";
|
|
|
80
86
|
export * from "./components/layout/sidebar-toggle-button.js";
|
|
81
87
|
export * from "./components/layout/split-view.js";
|
|
82
88
|
// Components: overlays
|
|
89
|
+
export * from "./components/overlays/context-menu.js";
|
|
83
90
|
export * from "./components/overlays/dialog.js";
|
|
84
91
|
export * from "./components/overlays/drawer.js";
|
|
85
92
|
export * from "./components/overlays/popover.js";
|
|
@@ -17,6 +17,10 @@ const elementStyles = `
|
|
|
17
17
|
display: block;
|
|
18
18
|
color: inherit;
|
|
19
19
|
font: inherit;
|
|
20
|
+
/* The table's header row is nowrap, so its min-content width is wide.
|
|
21
|
+
Without this the host refuses to shrink below that width and pushes
|
|
22
|
+
out of grid/flex containers instead of scrolling. */
|
|
23
|
+
min-width: 0;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
[part="table-shell"] {
|
|
@@ -24,7 +28,10 @@ const elementStyles = `
|
|
|
24
28
|
border-radius: ${boePanel.radius};
|
|
25
29
|
background: var(--boe-token-surface-surface, #ffffff);
|
|
26
30
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
27
|
-
|
|
31
|
+
/* Scroll the table within its own frame when the host is narrower
|
|
32
|
+
than the table's intrinsic width. */
|
|
33
|
+
overflow-x: auto;
|
|
34
|
+
overflow-y: hidden;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
[part="table-shell"][aria-busy="true"] {
|
|
@@ -13,6 +13,9 @@ const elementStyles = `
|
|
|
13
13
|
display: block;
|
|
14
14
|
color: inherit;
|
|
15
15
|
font: inherit;
|
|
16
|
+
/* Let the host shrink below its controls' intrinsic width when a
|
|
17
|
+
container constrains it. */
|
|
18
|
+
min-width: 0;
|
|
16
19
|
--_obp-surface: var(--boe-token-surface-surface, #ffffff);
|
|
17
20
|
--_obp-surface-muted: var(--boe-token-surface-surface-secondary, #fbfbfb);
|
|
18
21
|
--_obp-border: color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 82%, transparent);
|
|
@@ -38,9 +41,17 @@ const elementStyles = `
|
|
|
38
41
|
text-transform: uppercase;
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
/* Zero track floors, not fixed rem floors. The columns still size to
|
|
45
|
+
their content (so a shrink-to-fit host keeps the three-up layout at
|
|
46
|
+
its natural width) but can compress when a container constrains the
|
|
47
|
+
bar, instead of forcing the host wider than its container.
|
|
48
|
+
auto-fit was rejected: it collapses to one column whenever the host
|
|
49
|
+
is shrink-to-fit. A container query was too: container-type makes
|
|
50
|
+
the host's width independent of its content, which collapses the bar
|
|
51
|
+
anywhere it isn't explicitly stretched. */
|
|
41
52
|
[part="controls"] {
|
|
42
53
|
display: grid;
|
|
43
|
-
grid-template-columns: minmax(
|
|
54
|
+
grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 0.8fr));
|
|
44
55
|
gap: ${boePanel.gap};
|
|
45
56
|
align-items: center;
|
|
46
57
|
}
|
|
@@ -100,11 +111,6 @@ const elementStyles = `
|
|
|
100
111
|
color: var(--_obp-brand);
|
|
101
112
|
}
|
|
102
113
|
|
|
103
|
-
@media (max-width: 900px) {
|
|
104
|
-
[part="controls"] {
|
|
105
|
-
grid-template-columns: 1fr;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
114
|
`;
|
|
109
115
|
export class BoxFilterBarElement extends BaseElement {
|
|
110
116
|
static get observedAttributes() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unofficialbox/box-open-elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Box Open Elements: an open, framework-agnostic design system and web component library for Box-style experiences",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://unofficialbox.github.io/box-open-elements",
|