@fusion.dev/helix-core 0.171.0 → 0.172.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/custom-elements.json +84 -4
- package/dist/controllers/command-controller/command-controller.d.ts +37 -0
- package/dist/controllers/command-controller/command-controller.js +55 -1
- package/dist/controllers/floating-controller/floating-controller.d.ts +15 -1
- package/dist/controllers/floating-controller/floating-controller.js +13 -6
- package/dist/controllers/popover-controller/popover-controller.d.ts +6 -0
- package/dist/controllers/popover-controller/popover-controller.js +3 -0
- package/dist/mixins/mixin-picker-feedback/mixin-picker-feedback.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
package/custom-elements.json
CHANGED
|
@@ -983,12 +983,83 @@
|
|
|
983
983
|
"readonly": true,
|
|
984
984
|
"default": "options"
|
|
985
985
|
},
|
|
986
|
+
{
|
|
987
|
+
"kind": "field",
|
|
988
|
+
"name": "invokers",
|
|
989
|
+
"privacy": "private",
|
|
990
|
+
"readonly": true,
|
|
991
|
+
"default": "new Set<Element>()",
|
|
992
|
+
"description": "The invokers this host tells whether it is open: each one that sent an\nopening command, unless it carried `aria-expanded` of its own before then."
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
"kind": "field",
|
|
996
|
+
"name": "theirs",
|
|
997
|
+
"privacy": "private",
|
|
998
|
+
"readonly": true,
|
|
999
|
+
"default": "new WeakSet<Element>()",
|
|
1000
|
+
"description": "Invokers that carried `aria-expanded` before a command, which stay the consumer's."
|
|
1001
|
+
},
|
|
986
1002
|
{
|
|
987
1003
|
"kind": "field",
|
|
988
1004
|
"name": "handle",
|
|
989
1005
|
"privacy": "private",
|
|
990
1006
|
"readonly": true
|
|
991
1007
|
},
|
|
1008
|
+
{
|
|
1009
|
+
"kind": "method",
|
|
1010
|
+
"name": "note",
|
|
1011
|
+
"privacy": "private",
|
|
1012
|
+
"return": {
|
|
1013
|
+
"type": {
|
|
1014
|
+
"text": "void"
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
"parameters": [
|
|
1018
|
+
{
|
|
1019
|
+
"name": "source",
|
|
1020
|
+
"type": {
|
|
1021
|
+
"text": "Element | null"
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
],
|
|
1025
|
+
"description": "Takes the invoker on as one to tell, for a host that opens.\n\n**The platform says it for a native popover and not for this.** Chromium\nexposes a `popovertarget` button, or one with `command=\"toggle-popover\"`,\nas expanded or collapsed, and a custom command reaches a custom element\nwith nothing said. Measured before this: a `command=\"--toggle\"` button\nopened a Popover and its node had no expanded state. React Aria's trigger\nsays `aria-expanded` on every overlay it opens.\n\nFrom the first command on, so nothing is written onto markup a server\nsent until a reader presses. A plain invoker is also pointed at the host\nthrough `ariaControlsElements`, which writes no attribute."
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
"kind": "method",
|
|
1029
|
+
"name": "tell",
|
|
1030
|
+
"privacy": "private",
|
|
1031
|
+
"return": {
|
|
1032
|
+
"type": {
|
|
1033
|
+
"text": "void"
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
"description": "Writes the host's state on each invoker it tells, forgetting one that has left."
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
"kind": "method",
|
|
1040
|
+
"name": "write",
|
|
1041
|
+
"privacy": "private",
|
|
1042
|
+
"return": {
|
|
1043
|
+
"type": {
|
|
1044
|
+
"text": "void"
|
|
1045
|
+
}
|
|
1046
|
+
},
|
|
1047
|
+
"parameters": [
|
|
1048
|
+
{
|
|
1049
|
+
"name": "invoker",
|
|
1050
|
+
"type": {
|
|
1051
|
+
"text": "Element"
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
"name": "open",
|
|
1056
|
+
"type": {
|
|
1057
|
+
"text": "boolean"
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
],
|
|
1061
|
+
"description": "Says `open` on one invoker: a Helix control through its `expanded`\nproperty, which its own button carries and which puts nothing in the\nmarkup, and a plain element through `aria-expanded`."
|
|
1062
|
+
},
|
|
992
1063
|
{
|
|
993
1064
|
"kind": "method",
|
|
994
1065
|
"name": "hostConnected",
|
|
@@ -998,6 +1069,15 @@
|
|
|
998
1069
|
}
|
|
999
1070
|
}
|
|
1000
1071
|
},
|
|
1072
|
+
{
|
|
1073
|
+
"kind": "method",
|
|
1074
|
+
"name": "hostUpdated",
|
|
1075
|
+
"return": {
|
|
1076
|
+
"type": {
|
|
1077
|
+
"text": "void"
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1001
1081
|
{
|
|
1002
1082
|
"kind": "method",
|
|
1003
1083
|
"name": "hostDisconnected",
|
|
@@ -3923,7 +4003,7 @@
|
|
|
3923
4003
|
},
|
|
3924
4004
|
"privacy": "private",
|
|
3925
4005
|
"readonly": true,
|
|
3926
|
-
"default": "new FloatingController(host, { anchor: options.anchor, ...(options.arrow === undefined ? {} : { arrow: options.arrow }), floating: options.surface, ...(options.offset === undefined ? {} : { offset: options.offset }), ...(options.onPlace === undefined ? {} : { onPlace: options.onPlace }), ...(options.placement === undefined ? {} : { placement: options.placement }), ...(options.within === undefined ? {} : { within: options.within }), })"
|
|
4006
|
+
"default": "new FloatingController(host, { anchor: options.anchor, ...(options.arrow === undefined ? {} : { arrow: options.arrow }), floating: options.surface, ...(options.offset === undefined ? {} : { offset: options.offset }), ...(options.crossOffset === undefined ? {} : { crossOffset: options.crossOffset }), ...(options.flip === undefined ? {} : { flip: options.flip }), ...(options.padding === undefined ? {} : { padding: options.padding }), ...(options.onPlace === undefined ? {} : { onPlace: options.onPlace }), ...(options.placement === undefined ? {} : { placement: options.placement }), ...(options.within === undefined ? {} : { within: options.within }), })"
|
|
3927
4007
|
},
|
|
3928
4008
|
{
|
|
3929
4009
|
"kind": "field",
|
|
@@ -21133,7 +21213,7 @@
|
|
|
21133
21213
|
"text": "TemplateResult | typeof nothing"
|
|
21134
21214
|
}
|
|
21135
21215
|
},
|
|
21136
|
-
"description": "The `feedback` line, or nothing while there is none."
|
|
21216
|
+
"description": "The `feedback` line, or nothing while there is none.\n\n`role=\"alert\"`, as a field's feedback line has, so the message is\nannounced when it appears and the label and hint are not read with it."
|
|
21137
21217
|
}
|
|
21138
21218
|
],
|
|
21139
21219
|
"attributes": [
|
|
@@ -33711,7 +33791,7 @@
|
|
|
33711
33791
|
"type": {
|
|
33712
33792
|
"text": "string"
|
|
33713
33793
|
},
|
|
33714
|
-
"default": "'0.
|
|
33794
|
+
"default": "'0.172.0'",
|
|
33715
33795
|
"description": "The version of the package this file ships in.\n\n**Written by `fusion/scripts/version.mjs`** wherever it writes the package's\n`package.json`, and stamped back into the repository beside it after a\nrelease, so the two never disagree; a spec beside this holds them to it.\nNothing else in the running code knows which release it is."
|
|
33716
33796
|
}
|
|
33717
33797
|
],
|
|
@@ -34271,6 +34351,6 @@
|
|
|
34271
34351
|
],
|
|
34272
34352
|
"helix": {
|
|
34273
34353
|
"package": "@fusion.dev/helix-core",
|
|
34274
|
-
"version": "0.
|
|
34354
|
+
"version": "0.172.0"
|
|
34275
34355
|
}
|
|
34276
34356
|
}
|
|
@@ -40,6 +40,12 @@ export declare class HelixFallbackCommandEvent extends Event {
|
|
|
40
40
|
export interface CommandOptions {
|
|
41
41
|
/** Each command the host implements, keyed by the name a trigger writes. */
|
|
42
42
|
commands: Record<string, (source: Element | null) => void>;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the host is open, for a host a command opens and closes. Given,
|
|
45
|
+
* each invoker that has sent `--show` or `--toggle` says so with
|
|
46
|
+
* `aria-expanded`.
|
|
47
|
+
*/
|
|
48
|
+
expanded?: () => boolean;
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
51
|
* Lets a control elsewhere on the page operate this one, declaratively.
|
|
@@ -75,7 +81,38 @@ export declare class CommandController implements ReactiveController {
|
|
|
75
81
|
private readonly host;
|
|
76
82
|
private readonly options;
|
|
77
83
|
constructor(host: ReactiveControllerHost & HTMLElement, options: CommandOptions);
|
|
84
|
+
/**
|
|
85
|
+
* The invokers this host tells whether it is open: each one that sent an
|
|
86
|
+
* opening command, unless it carried `aria-expanded` of its own before then.
|
|
87
|
+
*/
|
|
88
|
+
private readonly invokers;
|
|
89
|
+
/** Invokers that carried `aria-expanded` before a command, which stay the consumer's. */
|
|
90
|
+
private readonly theirs;
|
|
78
91
|
private readonly handle;
|
|
92
|
+
/**
|
|
93
|
+
* Takes the invoker on as one to tell, for a host that opens.
|
|
94
|
+
*
|
|
95
|
+
* **The platform says it for a native popover and not for this.** Chromium
|
|
96
|
+
* exposes a `popovertarget` button, or one with `command="toggle-popover"`,
|
|
97
|
+
* as expanded or collapsed, and a custom command reaches a custom element
|
|
98
|
+
* with nothing said. Measured before this: a `command="--toggle"` button
|
|
99
|
+
* opened a Popover and its node had no expanded state. React Aria's trigger
|
|
100
|
+
* says `aria-expanded` on every overlay it opens.
|
|
101
|
+
*
|
|
102
|
+
* From the first command on, so nothing is written onto markup a server
|
|
103
|
+
* sent until a reader presses. A plain invoker is also pointed at the host
|
|
104
|
+
* through `ariaControlsElements`, which writes no attribute.
|
|
105
|
+
*/
|
|
106
|
+
private note;
|
|
107
|
+
/** Writes the host's state on each invoker it tells, forgetting one that has left. */
|
|
108
|
+
private tell;
|
|
109
|
+
/**
|
|
110
|
+
* Says `open` on one invoker: a Helix control through its `expanded`
|
|
111
|
+
* property, which its own button carries and which puts nothing in the
|
|
112
|
+
* markup, and a plain element through `aria-expanded`.
|
|
113
|
+
*/
|
|
114
|
+
private write;
|
|
79
115
|
hostConnected(): void;
|
|
116
|
+
hostUpdated(): void;
|
|
80
117
|
hostDisconnected(): void;
|
|
81
118
|
}
|
|
@@ -6,24 +6,78 @@ export class HelixFallbackCommandEvent extends Event {
|
|
|
6
6
|
this.source = source;
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
const OPENING = new Set(['--show', '--toggle']);
|
|
9
10
|
export class CommandController {
|
|
10
11
|
constructor(host, options) {
|
|
12
|
+
this.invokers = new Set();
|
|
13
|
+
this.theirs = new WeakSet();
|
|
11
14
|
this.handle = (event) => {
|
|
12
15
|
const command = event.command;
|
|
13
16
|
const run = this.options.commands[command];
|
|
14
17
|
if (!run) {
|
|
15
18
|
return;
|
|
16
19
|
}
|
|
17
|
-
|
|
20
|
+
const source = event.source;
|
|
21
|
+
if (OPENING.has(command)) {
|
|
22
|
+
this.note(source);
|
|
23
|
+
}
|
|
24
|
+
run(source);
|
|
18
25
|
};
|
|
19
26
|
this.host = host;
|
|
20
27
|
this.options = options;
|
|
21
28
|
host.addController(this);
|
|
22
29
|
}
|
|
30
|
+
note(source) {
|
|
31
|
+
if (!this.options.expanded || !source || this.invokers.has(source) || this.theirs.has(source)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (source.hasAttribute('aria-expanded')) {
|
|
35
|
+
this.theirs.add(source);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.invokers.add(source);
|
|
39
|
+
if (!source.localName.includes('-') && 'ariaControlsElements' in source) {
|
|
40
|
+
;
|
|
41
|
+
source.ariaControlsElements = [
|
|
42
|
+
this.host,
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
this.tell();
|
|
46
|
+
}
|
|
47
|
+
tell() {
|
|
48
|
+
const expanded = this.options.expanded;
|
|
49
|
+
if (!expanded) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const open = expanded();
|
|
53
|
+
for (const invoker of this.invokers) {
|
|
54
|
+
if (invoker.isConnected) {
|
|
55
|
+
this.write(invoker, open);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.invokers.delete(invoker);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
write(invoker, open) {
|
|
63
|
+
if (invoker.localName.includes('-')) {
|
|
64
|
+
;
|
|
65
|
+
invoker.expanded = open;
|
|
66
|
+
}
|
|
67
|
+
else if (invoker.getAttribute('aria-expanded') !== String(open)) {
|
|
68
|
+
invoker.setAttribute('aria-expanded', String(open));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
23
71
|
hostConnected() {
|
|
24
72
|
this.host.addEventListener('command', this.handle);
|
|
25
73
|
}
|
|
74
|
+
hostUpdated() {
|
|
75
|
+
this.tell();
|
|
76
|
+
}
|
|
26
77
|
hostDisconnected() {
|
|
27
78
|
this.host.removeEventListener('command', this.handle);
|
|
79
|
+
for (const invoker of this.invokers) {
|
|
80
|
+
this.write(invoker, false);
|
|
81
|
+
}
|
|
28
82
|
}
|
|
29
83
|
}
|
|
@@ -25,6 +25,13 @@ export type Placement = 'bottom' | 'bottom-end' | 'bottom-start' | 'left' | 'lef
|
|
|
25
25
|
export interface FloatingOptions {
|
|
26
26
|
/** The element being pointed at. */
|
|
27
27
|
anchor: () => HTMLElement | null | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* How far to slide the floating element along the side it is placed on, in
|
|
30
|
+
* pixels: rightward on the top or the bottom, downward on the left or the
|
|
31
|
+
* right, and toward the end of the line where the placement names `-end`.
|
|
32
|
+
* Floating UI's `crossAxis` offset, React Aria's `crossOffset`.
|
|
33
|
+
*/
|
|
34
|
+
crossOffset?: number | (() => number);
|
|
28
35
|
/**
|
|
29
36
|
* An element to point at the anchor, positioned against whichever side the
|
|
30
37
|
* floating element landed on.
|
|
@@ -32,13 +39,20 @@ export interface FloatingOptions {
|
|
|
32
39
|
arrow?: () => HTMLElement | null | undefined;
|
|
33
40
|
/** The element being placed. */
|
|
34
41
|
floating: () => HTMLElement | null | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* How close to the window's edges the floating element may come, in
|
|
44
|
+
* pixels, when it slides or is capped to fit, and, where it is given, when
|
|
45
|
+
* it flips. Defaults to 8 for the first two and none for flipping. React
|
|
46
|
+
* Aria's `containerPadding`, Radix's `collisionPadding`.
|
|
47
|
+
*/
|
|
48
|
+
padding?: number | (() => number | undefined);
|
|
35
49
|
/**
|
|
36
50
|
* Whether to try another side when the preferred one does not fit.
|
|
37
51
|
*
|
|
38
52
|
* On by default. A menu that opens off the bottom of the window is one the
|
|
39
53
|
* reader cannot use.
|
|
40
54
|
*/
|
|
41
|
-
flip?: boolean;
|
|
55
|
+
flip?: boolean | (() => boolean);
|
|
42
56
|
/**
|
|
43
57
|
* Told the placement the element landed on, after every measurement, and
|
|
44
58
|
* how wide the window leaves it room to be.
|
|
@@ -37,17 +37,24 @@ export class FloatingController {
|
|
|
37
37
|
const preferred = typeof this.options.placement === 'function'
|
|
38
38
|
? this.options.placement()
|
|
39
39
|
: this.options.placement;
|
|
40
|
-
const
|
|
40
|
+
const cross = typeof this.options.crossOffset === 'function'
|
|
41
|
+
? this.options.crossOffset()
|
|
42
|
+
: (this.options.crossOffset ?? 0);
|
|
43
|
+
const asked = typeof this.options.padding === 'function' ? this.options.padding() : this.options.padding;
|
|
44
|
+
const margin = asked ?? 8;
|
|
45
|
+
const edge = asked === undefined ? {} : { padding: asked };
|
|
46
|
+
const middleware = [offset({ crossAxis: cross, mainAxis: gap ?? 0 })];
|
|
41
47
|
let room;
|
|
42
|
-
|
|
48
|
+
const flips = typeof this.options.flip === 'function' ? this.options.flip() : this.options.flip;
|
|
49
|
+
if (flips !== false) {
|
|
43
50
|
const side = (preferred ?? 'bottom-start').split('-')[0];
|
|
44
51
|
middleware.push(flip(side === 'left' || side === 'right'
|
|
45
|
-
? { crossAxis: 'alignment', fallbackAxisSideDirection: 'end' }
|
|
46
|
-
:
|
|
52
|
+
? { crossAxis: 'alignment', fallbackAxisSideDirection: 'end', ...edge }
|
|
53
|
+
: edge));
|
|
47
54
|
}
|
|
48
55
|
if (this.options.shift !== false) {
|
|
49
56
|
const inside = this.options.within?.();
|
|
50
|
-
middleware.push(shift({ padding:
|
|
57
|
+
middleware.push(shift({ padding: margin, ...(inside ? { boundary: inside } : {}) }));
|
|
51
58
|
}
|
|
52
59
|
if (this.options.resize !== false) {
|
|
53
60
|
middleware.push(size({
|
|
@@ -56,7 +63,7 @@ export class FloatingController {
|
|
|
56
63
|
elements.floating.style.setProperty('--floating-available-block-size', `${Math.round(availableHeight)}px`);
|
|
57
64
|
elements.floating.style.setProperty('--floating-anchor-inline-size', `${matchedInlineSize(rects.reference.width, availableWidth)}px`);
|
|
58
65
|
},
|
|
59
|
-
padding:
|
|
66
|
+
padding: margin,
|
|
60
67
|
}));
|
|
61
68
|
}
|
|
62
69
|
if (pointer) {
|
|
@@ -21,6 +21,12 @@ export interface PopoverOptions {
|
|
|
21
21
|
boundary?: () => Element | readonly (Element | null | undefined)[] | null | undefined;
|
|
22
22
|
/** Space between the anchor and the surface, in pixels. */
|
|
23
23
|
offset?: number | (() => number);
|
|
24
|
+
/** Whether the surface may move to the other side to fit. On by default. See `FloatingController`. */
|
|
25
|
+
flip?: boolean | (() => boolean);
|
|
26
|
+
/** How far to slide the surface along its side, in pixels. See `FloatingController`. */
|
|
27
|
+
crossOffset?: number | (() => number);
|
|
28
|
+
/** How close to the window's edges the surface may come, in pixels. See `FloatingController`. */
|
|
29
|
+
padding?: number | (() => number | undefined);
|
|
24
30
|
/**
|
|
25
31
|
* A box to keep the surface inside, instead of the viewport.
|
|
26
32
|
*
|
|
@@ -10,6 +10,9 @@ export class PopoverController {
|
|
|
10
10
|
...(options.arrow === undefined ? {} : { arrow: options.arrow }),
|
|
11
11
|
floating: options.surface,
|
|
12
12
|
...(options.offset === undefined ? {} : { offset: options.offset }),
|
|
13
|
+
...(options.crossOffset === undefined ? {} : { crossOffset: options.crossOffset }),
|
|
14
|
+
...(options.flip === undefined ? {} : { flip: options.flip }),
|
|
15
|
+
...(options.padding === undefined ? {} : { padding: options.padding }),
|
|
13
16
|
...(options.onPlace === undefined ? {} : { onPlace: options.onPlace }),
|
|
14
17
|
...(options.placement === undefined ? {} : { placement: options.placement }),
|
|
15
18
|
...(options.within === undefined ? {} : { within: options.within }),
|
|
@@ -57,7 +57,7 @@ export const MixinPickerFeedback = (Base) => {
|
|
|
57
57
|
renderPickerFeedback() {
|
|
58
58
|
return this.feedback === ''
|
|
59
59
|
? nothing
|
|
60
|
-
: html `<p part="feedback" id="feedback">${this.feedback}</p>`;
|
|
60
|
+
: html `<p part="feedback" id="feedback" role="alert">${this.feedback}</p>`;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
__decorate([
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.172.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-core",
|
|
3
3
|
"description": "Helix element primitives: base classes, mixins, decorators, utilities.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.172.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@floating-ui/dom": "^1.8.0",
|
|
65
|
-
"@fusion.dev/helix-icons": "0.
|
|
65
|
+
"@fusion.dev/helix-icons": "0.172.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@fusion.dev/helix-cem": "0.
|
|
69
|
-
"@fusion.dev/helix-testing": "0.
|
|
68
|
+
"@fusion.dev/helix-cem": "0.172.0",
|
|
69
|
+
"@fusion.dev/helix-testing": "0.172.0",
|
|
70
70
|
"@lit-labs/ssr": "^4.1.0",
|
|
71
71
|
"lit": "^3.2.1"
|
|
72
72
|
},
|