@fusion.dev/helix-headless 0.14.0 → 0.15.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
CHANGED
|
@@ -26638,7 +26638,7 @@
|
|
|
26638
26638
|
"declarations": [
|
|
26639
26639
|
{
|
|
26640
26640
|
"kind": "class",
|
|
26641
|
-
"description": "**The tokens are the consumer's own elements, slotted.** What a filter narrows\nby belongs to the consumer's model rather than to this element: a facet with a\nprefix, a saved query, a range. So they are slotted rather than rendered from\na list of strings, and removing one is a request this element makes rather\nthan a change it applies. Combobox is the other way round, and that is the\ndifference between the two: its tokens are a view of a selection it owns,\ndrawn from options it was handed.\n\n**`value` is the text being typed, not the tokens**, which is what makes this\na field a consumer can listen to. Every keystroke announces `input` with the\ntext on the element, so a table narrows as the reader types. The tokens need\nno reporting back, being markup the consumer already holds.\n\nBackspace on an empty field asks the last token to go, through the same\n`helix-dismiss` its own remove control fires, so a consumer who handled the\npointer has already handled the keyboard.",
|
|
26641
|
+
"description": "**The tokens are the consumer's own elements, slotted.** What a filter narrows\nby belongs to the consumer's model rather than to this element: a facet with a\nprefix, a saved query, a range. So they are slotted rather than rendered from\na list of strings, and removing one is a request this element makes rather\nthan a change it applies. Combobox is the other way round, and that is the\ndifference between the two: its tokens are a view of a selection it owns,\ndrawn from options it was handed.\n\n**`value` is the text being typed, not the tokens**, which is what makes this\na field a consumer can listen to. Every keystroke announces `input` with the\ntext on the element, so a table narrows as the reader types. The tokens need\nno reporting back, being markup the consumer already holds.\n\nBackspace on an empty field asks the last token to go, through the same\n`helix-dismiss` its own remove control fires, so a consumer who handled the\npointer has already handled the keyboard.\n\n**It can suggest what to type next, and it never learns the grammar.** Slot\n`helix-option` elements into `suggestions` and the field shows them, moves\nthrough them with the arrows, and lets Enter press one; the option fires its\nown `helix-select` and the consumer decides what choosing meant. Deciding\n*what* to suggest is theirs too: keys before a colon and values after it are\nthe same panel with different contents, which is all a two-level filter is,\nand the parse that tells them apart belongs where the domain does.\n\n**The panel opens when there is something in it and closes when there is\nnot**, rather than on a choice. A reader who picks a key wants its values\nnext, and closing on the choice would shut the panel between two renders. Use\n`selectionStart` and `setSelectionRange` to write a completion back and put\nthe caret after it, or the next parse reads the wrong term.",
|
|
26642
26642
|
"name": "HelixHeadlessFilterInput",
|
|
26643
26643
|
"cssParts": [
|
|
26644
26644
|
{
|
|
@@ -26669,6 +26669,10 @@
|
|
|
26669
26669
|
"description": "How many tokens did not fit. A `<button>` with `expandable`, a `<span>` without it.",
|
|
26670
26670
|
"name": "rest"
|
|
26671
26671
|
},
|
|
26672
|
+
{
|
|
26673
|
+
"description": "The floating panel the suggestions sit in.",
|
|
26674
|
+
"name": "surface"
|
|
26675
|
+
},
|
|
26672
26676
|
{
|
|
26673
26677
|
"description": "The row the tokens sit in.",
|
|
26674
26678
|
"name": "tags"
|
|
@@ -26683,6 +26687,10 @@
|
|
|
26683
26687
|
"description": "Before the text, for a search icon.",
|
|
26684
26688
|
"name": "start"
|
|
26685
26689
|
},
|
|
26690
|
+
{
|
|
26691
|
+
"description": "What could be typed next, as `Option`s, and `Option.Group`s of them.",
|
|
26692
|
+
"name": "suggestions"
|
|
26693
|
+
},
|
|
26686
26694
|
{
|
|
26687
26695
|
"description": "What is already narrowing the results, as `Tag`s.",
|
|
26688
26696
|
"name": "tags"
|
|
@@ -26751,6 +26759,43 @@
|
|
|
26751
26759
|
"default": "0",
|
|
26752
26760
|
"description": "How many tokens did not fit, which is what the row's count renders."
|
|
26753
26761
|
},
|
|
26762
|
+
{
|
|
26763
|
+
"kind": "field",
|
|
26764
|
+
"name": "activeAt",
|
|
26765
|
+
"type": {
|
|
26766
|
+
"text": "number"
|
|
26767
|
+
},
|
|
26768
|
+
"privacy": "private",
|
|
26769
|
+
"default": "-1",
|
|
26770
|
+
"description": "Which suggestion the keyboard is on, or `-1` for none.\n\nAn index rather than a value, unlike Listbox's: these are the consumer's\nown list and two of them may legitimately carry the same value, a `label`\nand an `assignee` both called `ada`. Position is what a reader is moving\nthrough and it is what stays right when they do."
|
|
26771
|
+
},
|
|
26772
|
+
{
|
|
26773
|
+
"kind": "field",
|
|
26774
|
+
"name": "suggesting",
|
|
26775
|
+
"type": {
|
|
26776
|
+
"text": "boolean"
|
|
26777
|
+
},
|
|
26778
|
+
"privacy": "private",
|
|
26779
|
+
"default": "false",
|
|
26780
|
+
"description": "Whether anything is offering itself to be suggested.\n\n**The ARIA follows this rather than being declared once.** A text field\ncarrying `role=\"combobox\"` promises a popup; a filter field that no consumer\never gives options to has none, and saying otherwise sends a screen reader\nlooking for a list that does not exist. So the role, `aria-expanded` and\n`aria-controls` all arrive with the first option and leave with the last."
|
|
26781
|
+
},
|
|
26782
|
+
{
|
|
26783
|
+
"kind": "field",
|
|
26784
|
+
"name": "dismissed",
|
|
26785
|
+
"type": {
|
|
26786
|
+
"text": "boolean"
|
|
26787
|
+
},
|
|
26788
|
+
"privacy": "private",
|
|
26789
|
+
"default": "false",
|
|
26790
|
+
"description": "Whether Escape has closed the panel over text that still has suggestions.\n\nWithout it the panel is open whenever there is something to show, so Escape\nwould close it and the next render would put it straight back. Cleared by\nthe next keystroke, because that is the reader saying something new."
|
|
26791
|
+
},
|
|
26792
|
+
{
|
|
26793
|
+
"kind": "field",
|
|
26794
|
+
"name": "panel",
|
|
26795
|
+
"privacy": "protected",
|
|
26796
|
+
"readonly": true,
|
|
26797
|
+
"default": "new PopoverController(this, { anchor: () => this.renderRoot?.querySelector<HTMLElement>('[part=\"field\"]'), boundary: (): readonly (Element | null | undefined)[] => [ this.renderRoot?.querySelector<HTMLElement>('[part=\"surface\"]'), this.renderRoot?.querySelector<HTMLElement>('[part=\"field\"]'), ], offset: () => 4, onDismiss: () => this.requestOpen(false), placement: 'bottom-start', surface: () => this.renderRoot?.querySelector<HTMLElement>('[part=\"surface\"]'), })"
|
|
26798
|
+
},
|
|
26754
26799
|
{
|
|
26755
26800
|
"kind": "field",
|
|
26756
26801
|
"name": "releaseCaret",
|
|
@@ -26798,6 +26843,77 @@
|
|
|
26798
26843
|
"description": "Whether there is a clear button on screen right now.",
|
|
26799
26844
|
"readonly": true
|
|
26800
26845
|
},
|
|
26846
|
+
{
|
|
26847
|
+
"kind": "field",
|
|
26848
|
+
"name": "suggestions",
|
|
26849
|
+
"type": {
|
|
26850
|
+
"text": "SuggestionOption[]"
|
|
26851
|
+
},
|
|
26852
|
+
"privacy": "private",
|
|
26853
|
+
"description": "The suggestions, in the order a reader moves through them.\n\n**Groups are opened out rather than walked into.** A slot assigns the group\nelement, not the options inside it, so a panel that read only what was\nassigned would offer nothing at all the moment a consumer reached for\n`option-group` to say what a run of values belongs to.\n\nA disabled option keeps its place on screen and is skipped here, which is\nthe difference between a list that explains why something cannot be chosen\nand one that pretends it was never there.",
|
|
26854
|
+
"readonly": true
|
|
26855
|
+
},
|
|
26856
|
+
{
|
|
26857
|
+
"kind": "field",
|
|
26858
|
+
"name": "selectionDirection",
|
|
26859
|
+
"type": {
|
|
26860
|
+
"text": "'backward' | 'forward' | 'none'"
|
|
26861
|
+
},
|
|
26862
|
+
"description": "The caret and the selection, under the platform's own names.\n\n**Published for the reason `Input` publishes them, and this element needs\nthem more.** The `<input>` is inside this shadow root, so a consumer has no\nway to ask where the caret is: `querySelector` does not cross the boundary.\nA grammar that completes what is being typed cannot work without it, because\n\"what is being typed\" is the term the caret is in, and after writing a\ncompletion back the caret has to be put after it or the next parse reads the\nwrong term.\n\nZero before there is a control, which is where an empty field's caret would\nbe anyway.",
|
|
26863
|
+
"readonly": true
|
|
26864
|
+
},
|
|
26865
|
+
{
|
|
26866
|
+
"kind": "field",
|
|
26867
|
+
"name": "selectionEnd",
|
|
26868
|
+
"type": {
|
|
26869
|
+
"text": "number"
|
|
26870
|
+
},
|
|
26871
|
+
"description": "Where the selection ends, which is the caret when nothing is selected.",
|
|
26872
|
+
"readonly": true
|
|
26873
|
+
},
|
|
26874
|
+
{
|
|
26875
|
+
"kind": "field",
|
|
26876
|
+
"name": "selectionStart",
|
|
26877
|
+
"type": {
|
|
26878
|
+
"text": "number"
|
|
26879
|
+
},
|
|
26880
|
+
"description": "Where the selection starts, which is the caret when nothing is selected.",
|
|
26881
|
+
"readonly": true
|
|
26882
|
+
},
|
|
26883
|
+
{
|
|
26884
|
+
"kind": "method",
|
|
26885
|
+
"name": "setSelectionRange",
|
|
26886
|
+
"return": {
|
|
26887
|
+
"type": {
|
|
26888
|
+
"text": "void"
|
|
26889
|
+
}
|
|
26890
|
+
},
|
|
26891
|
+
"parameters": [
|
|
26892
|
+
{
|
|
26893
|
+
"name": "start",
|
|
26894
|
+
"type": {
|
|
26895
|
+
"text": "number"
|
|
26896
|
+
}
|
|
26897
|
+
},
|
|
26898
|
+
{
|
|
26899
|
+
"name": "end",
|
|
26900
|
+
"type": {
|
|
26901
|
+
"text": "number"
|
|
26902
|
+
}
|
|
26903
|
+
},
|
|
26904
|
+
{
|
|
26905
|
+
"name": "direction",
|
|
26906
|
+
"optional": true,
|
|
26907
|
+
"type": {
|
|
26908
|
+
"text": "'backward' | 'forward' | 'none'"
|
|
26909
|
+
}
|
|
26910
|
+
}
|
|
26911
|
+
],
|
|
26912
|
+
"description": "Moves the caret, or selects a run of the text.",
|
|
26913
|
+
"type": {
|
|
26914
|
+
"text": "setSelectionRange(start: number, end: number, direction?: 'backward' | 'forward' | 'none') => void"
|
|
26915
|
+
}
|
|
26916
|
+
},
|
|
26801
26917
|
{
|
|
26802
26918
|
"kind": "field",
|
|
26803
26919
|
"name": "handleInput",
|
|
@@ -26819,12 +26935,74 @@
|
|
|
26819
26935
|
"readonly": true,
|
|
26820
26936
|
"description": "Empties the text and gives the caret back.\n\nFocus returns to the input rather than staying on a button that is about to\ndisappear, which would otherwise land on the body."
|
|
26821
26937
|
},
|
|
26938
|
+
{
|
|
26939
|
+
"kind": "method",
|
|
26940
|
+
"name": "settleOpen",
|
|
26941
|
+
"privacy": "private",
|
|
26942
|
+
"return": {
|
|
26943
|
+
"type": {
|
|
26944
|
+
"text": "void"
|
|
26945
|
+
}
|
|
26946
|
+
},
|
|
26947
|
+
"description": "Opens the panel when there is something in it, and closes it when there is\nnot.\n\n**Derived rather than commanded, which is the whole of how two levels\nwork.** A reader who picks `disposition` wants its values next, and the\nconsumer answers by rendering those values; if picking closed the panel it\nwould shut and reopen between two renders, which a reader sees as a flicker\nand a screen reader hears as a list going away. Nothing here closes on a\nchoice: the panel goes when the consumer stops offering, which is exactly\nwhen a value has been completed and there is nothing left to say.\n\nThrough `requestOpen` for both directions, so `helix-before-open` and\n`helix-before-close` still gate it and a consumer who refuses is obeyed.\n**No guard of its own**, because that method already returns early when the\nanswer has not moved: this runs after every render, and a second guard here\nwould only be a copy of that one going stale."
|
|
26948
|
+
},
|
|
26949
|
+
{
|
|
26950
|
+
"kind": "method",
|
|
26951
|
+
"name": "move",
|
|
26952
|
+
"privacy": "private",
|
|
26953
|
+
"return": {
|
|
26954
|
+
"type": {
|
|
26955
|
+
"text": "void"
|
|
26956
|
+
}
|
|
26957
|
+
},
|
|
26958
|
+
"parameters": [
|
|
26959
|
+
{
|
|
26960
|
+
"name": "by",
|
|
26961
|
+
"type": {
|
|
26962
|
+
"text": "number"
|
|
26963
|
+
}
|
|
26964
|
+
}
|
|
26965
|
+
],
|
|
26966
|
+
"description": "Moves the highlight, wrapping, and opens the panel if it was closed."
|
|
26967
|
+
},
|
|
26822
26968
|
{
|
|
26823
26969
|
"kind": "field",
|
|
26824
26970
|
"name": "handleKeydown",
|
|
26825
26971
|
"privacy": "private",
|
|
26826
26972
|
"readonly": true
|
|
26827
26973
|
},
|
|
26974
|
+
{
|
|
26975
|
+
"kind": "method",
|
|
26976
|
+
"name": "refreshSuggesting",
|
|
26977
|
+
"privacy": "private",
|
|
26978
|
+
"return": {
|
|
26979
|
+
"type": {
|
|
26980
|
+
"text": "void"
|
|
26981
|
+
}
|
|
26982
|
+
},
|
|
26983
|
+
"description": "What is on offer, settled before the render that draws it.\n\n**Not left to `slotchange`, for the reason the token row is not.** That\nevent reports the assignment moving and nothing else, and it does not fire\nat all under the harness these are tested in, so a panel that learned only\nfrom it would be a panel that never opened. Reading the slot every update is\nwhat makes the answer true whenever anyone asks.\n\nState only. Opening is an announcement and belongs after the render, which\nis why `settleOpen` is not called from here."
|
|
26984
|
+
},
|
|
26985
|
+
{
|
|
26986
|
+
"kind": "field",
|
|
26987
|
+
"name": "handleSuggestions",
|
|
26988
|
+
"privacy": "private",
|
|
26989
|
+
"readonly": true,
|
|
26990
|
+
"description": "The panel again, whenever what is in it changes."
|
|
26991
|
+
},
|
|
26992
|
+
{
|
|
26993
|
+
"kind": "field",
|
|
26994
|
+
"name": "handleFocusIn",
|
|
26995
|
+
"privacy": "private",
|
|
26996
|
+
"readonly": true,
|
|
26997
|
+
"description": "Focus arriving in the field is a reader asking what they can type."
|
|
26998
|
+
},
|
|
26999
|
+
{
|
|
27000
|
+
"kind": "field",
|
|
27001
|
+
"name": "handleFocusOut",
|
|
27002
|
+
"privacy": "private",
|
|
27003
|
+
"readonly": true,
|
|
27004
|
+
"description": "Focus leaving the element closes it.\n\n`relatedTarget` and not a timer: a press inside the panel moves focus within\nthis element and must not close what is being pressed."
|
|
27005
|
+
},
|
|
26828
27006
|
{
|
|
26829
27007
|
"kind": "field",
|
|
26830
27008
|
"name": "handleTokens",
|
|
@@ -26847,6 +27025,17 @@
|
|
|
26847
27025
|
"module": "src/mixins/mixin-tokens/mixin-tokens.ts"
|
|
26848
27026
|
}
|
|
26849
27027
|
},
|
|
27028
|
+
{
|
|
27029
|
+
"kind": "method",
|
|
27030
|
+
"name": "markActive",
|
|
27031
|
+
"privacy": "private",
|
|
27032
|
+
"return": {
|
|
27033
|
+
"type": {
|
|
27034
|
+
"text": "void"
|
|
27035
|
+
}
|
|
27036
|
+
},
|
|
27037
|
+
"description": "Marks the highlighted suggestion, and tells the control which one it is.\n\n**Two halves, and neither does the other's job.** `active` is the option's\nown published flag and is what a stylesheet draws from. The control carries\n`aria-activedescendant`, because focus never leaves the text: a screen\nreader is told what is highlighted by the field the caret is in, not by the\nlist.\n\nThrough `ariaActiveDescendantElement` rather than an id, which is what\n`Listbox` reaches for and for the same two reasons: it crosses a shadow\nboundary, where an `aria-activedescendant` naming an id cannot, and it needs\nno ids invented for the consumer's own elements. Feature-tested, because\nwhere it is missing the assignment is a silent no-op rather than an error,\nand the highlight is still drawn."
|
|
27038
|
+
},
|
|
26850
27039
|
{
|
|
26851
27040
|
"kind": "method",
|
|
26852
27041
|
"name": "focus",
|
|
@@ -26869,6 +27058,112 @@
|
|
|
26869
27058
|
"text": "focus(options?: FocusOptions) => void"
|
|
26870
27059
|
}
|
|
26871
27060
|
},
|
|
27061
|
+
{
|
|
27062
|
+
"kind": "field",
|
|
27063
|
+
"name": "open",
|
|
27064
|
+
"type": {
|
|
27065
|
+
"text": "boolean"
|
|
27066
|
+
},
|
|
27067
|
+
"description": "Whether the element is open.\n\nOne property, not two, so `dialog.open` answers the question anyone\nasking it meant. Writing always applies; `<helix-dialog open>` is the\nsame statement in markup.\n\n**Not reflected, and that is required rather than tidy.** Reflection\nwrites the resolved state back as an attribute, so an element that opened\nitself would put `open` on its own host: cloned, serialized for\nhydration, or diffed by a framework, it comes back holding whatever the\nlast reader did rather than what the author wrote. `:state(open)` carries\nthe resolved state for stylesheets, which is what the mirror is for.",
|
|
27068
|
+
"attribute": "open",
|
|
27069
|
+
"inheritedFrom": {
|
|
27070
|
+
"name": "MixinControllerOpen",
|
|
27071
|
+
"module": "src/mixins/mixin-controller-open/mixin-controller-open.ts"
|
|
27072
|
+
}
|
|
27073
|
+
},
|
|
27074
|
+
{
|
|
27075
|
+
"kind": "field",
|
|
27076
|
+
"name": "defaultOpen",
|
|
27077
|
+
"type": {
|
|
27078
|
+
"text": "boolean"
|
|
27079
|
+
},
|
|
27080
|
+
"default": "false",
|
|
27081
|
+
"description": "Initial open state for uncontrolled use, where nothing declared one.",
|
|
27082
|
+
"attribute": "default-open",
|
|
27083
|
+
"inheritedFrom": {
|
|
27084
|
+
"name": "MixinControllerOpen",
|
|
27085
|
+
"module": "src/mixins/mixin-controller-open/mixin-controller-open.ts"
|
|
27086
|
+
}
|
|
27087
|
+
},
|
|
27088
|
+
{
|
|
27089
|
+
"kind": "field",
|
|
27090
|
+
"name": "openState",
|
|
27091
|
+
"type": {
|
|
27092
|
+
"text": "boolean | undefined"
|
|
27093
|
+
},
|
|
27094
|
+
"privacy": "private",
|
|
27095
|
+
"default": "undefined",
|
|
27096
|
+
"description": "The state, whoever set it, and `undefined` until somebody has.\n\nPrefixed, and it has to be. These mixins compose onto one prototype\nchain, `Select` takes this one and `MixinControllerValue` together, and\nTypeScript's `private` is a compile-time fiction: two mixins with a field\ncalled `state` share one property on the instance, and the second one\nsilently overwrites the first. Measured, as a select whose `value` came\nback holding its `open` boolean.\n\nOne field and not two. Writing always applies, the way a native control\ndoes when a script assigns to it.",
|
|
27097
|
+
"inheritedFrom": {
|
|
27098
|
+
"name": "MixinControllerOpen",
|
|
27099
|
+
"module": "src/mixins/mixin-controller-open/mixin-controller-open.ts"
|
|
27100
|
+
}
|
|
27101
|
+
},
|
|
27102
|
+
{
|
|
27103
|
+
"kind": "method",
|
|
27104
|
+
"name": "requestOpen",
|
|
27105
|
+
"return": {
|
|
27106
|
+
"type": {
|
|
27107
|
+
"text": "boolean"
|
|
27108
|
+
}
|
|
27109
|
+
},
|
|
27110
|
+
"parameters": [
|
|
27111
|
+
{
|
|
27112
|
+
"name": "next",
|
|
27113
|
+
"type": {
|
|
27114
|
+
"text": "boolean"
|
|
27115
|
+
}
|
|
27116
|
+
}
|
|
27117
|
+
],
|
|
27118
|
+
"description": "Requests an open-state change, and reports whether it was applied.\n\nAnnounces before it acts, so `preventDefault()` on the `helix-before-open` or\n`helix-before-close` is a refusal and nothing moves. Nobody refusing means the\nelement opens or closes itself, whether or not a consumer declared\n`open`: a declared property is a seed and a correction, and a consumer\nwho wants the decision for themselves refuses it and assigns their own.",
|
|
27119
|
+
"inheritedFrom": {
|
|
27120
|
+
"name": "MixinControllerOpen",
|
|
27121
|
+
"module": "src/mixins/mixin-controller-open/mixin-controller-open.ts"
|
|
27122
|
+
},
|
|
27123
|
+
"type": {
|
|
27124
|
+
"text": "requestOpen(next: boolean) => boolean"
|
|
27125
|
+
}
|
|
27126
|
+
},
|
|
27127
|
+
{
|
|
27128
|
+
"kind": "method",
|
|
27129
|
+
"name": "announceToggle",
|
|
27130
|
+
"privacy": "private",
|
|
27131
|
+
"return": {
|
|
27132
|
+
"type": {
|
|
27133
|
+
"text": "void"
|
|
27134
|
+
}
|
|
27135
|
+
},
|
|
27136
|
+
"parameters": [
|
|
27137
|
+
{
|
|
27138
|
+
"name": "open",
|
|
27139
|
+
"type": {
|
|
27140
|
+
"text": "boolean"
|
|
27141
|
+
}
|
|
27142
|
+
}
|
|
27143
|
+
],
|
|
27144
|
+
"description": "Says the state moved, under the platform's own name and in its shape.\n\n**A native `toggle` from inside a shadow root reaches nobody**, because\nit neither bubbles nor composes, so the ten families showing a\n`popover=\"manual\"` panel and the two rendering a real `<dialog>` announce\nnothing to a consumer. The disclosures have no native element to fire\nanything at all: `AccordionItem` and `TreeItem` are `aria-expanded` and\n`hidden` written by hand, which is `<details>`'s job done manually.\n\n**From here rather than from `PopoverController`.** The controller covers\neleven families and the mixin covers sixteen, so announcing from both\nwould announce twice on the ten they share. What `toggle` reports is the\n*state*, and the state is this mixin's; the controller is a way of\nputting a panel on the screen.\n\n**`newState` and `oldState`, because that is the platform's shape**, so a\nconsumer arriving from `<details>` reads the same two words. Not\ncancelable: the before events are the request, and a second cancelable\nevent for one transition is two answers to one question.",
|
|
27145
|
+
"inheritedFrom": {
|
|
27146
|
+
"name": "MixinControllerOpen",
|
|
27147
|
+
"module": "src/mixins/mixin-controller-open/mixin-controller-open.ts"
|
|
27148
|
+
}
|
|
27149
|
+
},
|
|
27150
|
+
{
|
|
27151
|
+
"kind": "method",
|
|
27152
|
+
"name": "toggleOpen",
|
|
27153
|
+
"return": {
|
|
27154
|
+
"type": {
|
|
27155
|
+
"text": "boolean"
|
|
27156
|
+
}
|
|
27157
|
+
},
|
|
27158
|
+
"description": "Requests the opposite of the current state.",
|
|
27159
|
+
"inheritedFrom": {
|
|
27160
|
+
"name": "MixinControllerOpen",
|
|
27161
|
+
"module": "src/mixins/mixin-controller-open/mixin-controller-open.ts"
|
|
27162
|
+
},
|
|
27163
|
+
"type": {
|
|
27164
|
+
"text": "toggleOpen() => boolean"
|
|
27165
|
+
}
|
|
27166
|
+
},
|
|
26872
27167
|
{
|
|
26873
27168
|
"kind": "field",
|
|
26874
27169
|
"name": "collapseLabel",
|
|
@@ -27732,6 +28027,22 @@
|
|
|
27732
28027
|
"name": "helix-before-change",
|
|
27733
28028
|
"reactProp": "onhelix-before-change"
|
|
27734
28029
|
},
|
|
28030
|
+
{
|
|
28031
|
+
"type": {
|
|
28032
|
+
"text": "HelixBeforeCloseEvent"
|
|
28033
|
+
},
|
|
28034
|
+
"description": "Fired before the suggestions go, and cancelable.",
|
|
28035
|
+
"name": "helix-before-close",
|
|
28036
|
+
"reactProp": "onhelix-before-close"
|
|
28037
|
+
},
|
|
28038
|
+
{
|
|
28039
|
+
"type": {
|
|
28040
|
+
"text": "HelixBeforeOpenEvent"
|
|
28041
|
+
},
|
|
28042
|
+
"description": "Fired before the suggestions show, and cancelable.",
|
|
28043
|
+
"name": "helix-before-open",
|
|
28044
|
+
"reactProp": "onhelix-before-open"
|
|
28045
|
+
},
|
|
27735
28046
|
{
|
|
27736
28047
|
"name": "input",
|
|
27737
28048
|
"type": {
|
|
@@ -27743,6 +28054,14 @@
|
|
|
27743
28054
|
"module": "src/mixins/mixin-controller-value/mixin-controller-value.ts"
|
|
27744
28055
|
},
|
|
27745
28056
|
"reactProp": "oninput"
|
|
28057
|
+
},
|
|
28058
|
+
{
|
|
28059
|
+
"type": {
|
|
28060
|
+
"text": "Event"
|
|
28061
|
+
},
|
|
28062
|
+
"description": "Says the panel moved, carrying `newState` and `oldState`.",
|
|
28063
|
+
"name": "toggle",
|
|
28064
|
+
"reactProp": "ontoggle"
|
|
27746
28065
|
}
|
|
27747
28066
|
],
|
|
27748
28067
|
"attributes": [
|
|
@@ -27773,6 +28092,31 @@
|
|
|
27773
28092
|
"description": "Shown while the field is empty.\n\nNot a label: it goes when typing starts.",
|
|
27774
28093
|
"fieldName": "placeholder"
|
|
27775
28094
|
},
|
|
28095
|
+
{
|
|
28096
|
+
"name": "open",
|
|
28097
|
+
"type": {
|
|
28098
|
+
"text": "boolean"
|
|
28099
|
+
},
|
|
28100
|
+
"description": "Whether the element is open.\n\nOne property, not two, so `dialog.open` answers the question anyone\nasking it meant. Writing always applies; `<helix-dialog open>` is the\nsame statement in markup.\n\n**Not reflected, and that is required rather than tidy.** Reflection\nwrites the resolved state back as an attribute, so an element that opened\nitself would put `open` on its own host: cloned, serialized for\nhydration, or diffed by a framework, it comes back holding whatever the\nlast reader did rather than what the author wrote. `:state(open)` carries\nthe resolved state for stylesheets, which is what the mirror is for.",
|
|
28101
|
+
"fieldName": "open",
|
|
28102
|
+
"inheritedFrom": {
|
|
28103
|
+
"name": "MixinControllerOpen",
|
|
28104
|
+
"module": "src/mixins/mixin-controller-open/mixin-controller-open.ts"
|
|
28105
|
+
}
|
|
28106
|
+
},
|
|
28107
|
+
{
|
|
28108
|
+
"name": "default-open",
|
|
28109
|
+
"type": {
|
|
28110
|
+
"text": "boolean"
|
|
28111
|
+
},
|
|
28112
|
+
"default": "false",
|
|
28113
|
+
"description": "Initial open state for uncontrolled use, where nothing declared one.",
|
|
28114
|
+
"fieldName": "defaultOpen",
|
|
28115
|
+
"inheritedFrom": {
|
|
28116
|
+
"name": "MixinControllerOpen",
|
|
28117
|
+
"module": "src/mixins/mixin-controller-open/mixin-controller-open.ts"
|
|
28118
|
+
}
|
|
28119
|
+
},
|
|
27776
28120
|
{
|
|
27777
28121
|
"name": "collapse-label",
|
|
27778
28122
|
"type": {
|
|
@@ -27930,6 +28274,10 @@
|
|
|
27930
28274
|
}
|
|
27931
28275
|
],
|
|
27932
28276
|
"mixins": [
|
|
28277
|
+
{
|
|
28278
|
+
"name": "MixinControllerOpen",
|
|
28279
|
+
"package": "@fusion.dev/helix-core"
|
|
28280
|
+
},
|
|
27933
28281
|
{
|
|
27934
28282
|
"name": "MixinTokens",
|
|
27935
28283
|
"package": "@fusion.dev/helix-core"
|
|
@@ -53,7 +53,14 @@ registerElementEvents({
|
|
|
53
53
|
'helix-headless-drilldown': ['helix-select'],
|
|
54
54
|
'helix-headless-drilldown-item': ['helix-select'],
|
|
55
55
|
'helix-headless-file-input': ['change', 'helix-before-change', 'input'],
|
|
56
|
-
'helix-headless-filter-input': [
|
|
56
|
+
'helix-headless-filter-input': [
|
|
57
|
+
'change',
|
|
58
|
+
'helix-before-change',
|
|
59
|
+
'helix-before-close',
|
|
60
|
+
'helix-before-open',
|
|
61
|
+
'input',
|
|
62
|
+
'toggle',
|
|
63
|
+
],
|
|
57
64
|
'helix-headless-heatmap': ['helix-resize'],
|
|
58
65
|
'helix-headless-image': ['error', 'load'],
|
|
59
66
|
'helix-headless-input': ['change', 'helix-before-change', 'input'],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AriaForwardController, HelixFormElement } from '@fusion.dev/helix-core';
|
|
1
|
+
import { AriaForwardController, HelixFormElement, PopoverController } from '@fusion.dev/helix-core';
|
|
2
2
|
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
|
-
declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinTokensInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
|
|
3
|
+
declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinTokensInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
|
|
4
4
|
/**
|
|
5
5
|
* @summary A field that narrows something else, beside what is already narrowing it.
|
|
6
6
|
*
|
|
@@ -22,6 +22,20 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
22
22
|
* `helix-dismiss` its own remove control fires, so a consumer who handled the
|
|
23
23
|
* pointer has already handled the keyboard.
|
|
24
24
|
*
|
|
25
|
+
* **It can suggest what to type next, and it never learns the grammar.** Slot
|
|
26
|
+
* `helix-option` elements into `suggestions` and the field shows them, moves
|
|
27
|
+
* through them with the arrows, and lets Enter press one; the option fires its
|
|
28
|
+
* own `helix-select` and the consumer decides what choosing meant. Deciding
|
|
29
|
+
* *what* to suggest is theirs too: keys before a colon and values after it are
|
|
30
|
+
* the same panel with different contents, which is all a two-level filter is,
|
|
31
|
+
* and the parse that tells them apart belongs where the domain does.
|
|
32
|
+
*
|
|
33
|
+
* **The panel opens when there is something in it and closes when there is
|
|
34
|
+
* not**, rather than on a choice. A reader who picks a key wants its values
|
|
35
|
+
* next, and closing on the choice would shut the panel between two renders. Use
|
|
36
|
+
* `selectionStart` and `setSelectionRange` to write a completion back and put
|
|
37
|
+
* the caret after it, or the next parse reads the wrong term.
|
|
38
|
+
*
|
|
25
39
|
* @since 0.1.0
|
|
26
40
|
* @status experimental
|
|
27
41
|
* @category forms
|
|
@@ -30,11 +44,15 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
30
44
|
*
|
|
31
45
|
* @slot end - After the text, for a count or a control.
|
|
32
46
|
* @slot start - Before the text, for a search icon.
|
|
47
|
+
* @slot suggestions - What could be typed next, as `Option`s, and `Option.Group`s of them.
|
|
33
48
|
* @slot tags - What is already narrowing the results, as `Tag`s.
|
|
34
49
|
*
|
|
35
50
|
* @event {Event} change - Relayed from the input once the reader has finished.
|
|
36
51
|
* @event {HelixBeforeChangeEvent} helix-before-change - Fired as the text changes, and cancelable.
|
|
52
|
+
* @event {HelixBeforeCloseEvent} helix-before-close - Fired before the suggestions go, and cancelable.
|
|
53
|
+
* @event {HelixBeforeOpenEvent} helix-before-open - Fired before the suggestions show, and cancelable.
|
|
37
54
|
* @event {Event} input - Announced once the text has moved.
|
|
55
|
+
* @event {Event} toggle - Says the panel moved, carrying `newState` and `oldState`.
|
|
38
56
|
*
|
|
39
57
|
* @csspart clear - The button that empties the field.
|
|
40
58
|
* @csspart control - The input itself.
|
|
@@ -43,6 +61,7 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
43
61
|
* @csspart hint - Help under the field, while nothing is wrong.
|
|
44
62
|
* @csspart label - The question, over the field.
|
|
45
63
|
* @csspart rest - How many tokens did not fit. A `<button>` with `expandable`, a `<span>` without it.
|
|
64
|
+
* @csspart surface - The floating panel the suggestions sit in.
|
|
46
65
|
* @csspart tags - The row the tokens sit in.
|
|
47
66
|
*/
|
|
48
67
|
export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_base {
|
|
@@ -70,6 +89,34 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
70
89
|
placeholder: string;
|
|
71
90
|
/** How many tokens did not fit, which is what the row's count renders. */
|
|
72
91
|
private over;
|
|
92
|
+
/**
|
|
93
|
+
* Which suggestion the keyboard is on, or `-1` for none.
|
|
94
|
+
*
|
|
95
|
+
* An index rather than a value, unlike Listbox's: these are the consumer's
|
|
96
|
+
* own list and two of them may legitimately carry the same value, a `label`
|
|
97
|
+
* and an `assignee` both called `ada`. Position is what a reader is moving
|
|
98
|
+
* through and it is what stays right when they do.
|
|
99
|
+
*/
|
|
100
|
+
private activeAt;
|
|
101
|
+
/**
|
|
102
|
+
* Whether anything is offering itself to be suggested.
|
|
103
|
+
*
|
|
104
|
+
* **The ARIA follows this rather than being declared once.** A text field
|
|
105
|
+
* carrying `role="combobox"` promises a popup; a filter field that no consumer
|
|
106
|
+
* ever gives options to has none, and saying otherwise sends a screen reader
|
|
107
|
+
* looking for a list that does not exist. So the role, `aria-expanded` and
|
|
108
|
+
* `aria-controls` all arrive with the first option and leave with the last.
|
|
109
|
+
*/
|
|
110
|
+
private suggesting;
|
|
111
|
+
/**
|
|
112
|
+
* Whether Escape has closed the panel over text that still has suggestions.
|
|
113
|
+
*
|
|
114
|
+
* Without it the panel is open whenever there is something to show, so Escape
|
|
115
|
+
* would close it and the next render would put it straight back. Cleared by
|
|
116
|
+
* the next keystroke, because that is the reader saying something new.
|
|
117
|
+
*/
|
|
118
|
+
private dismissed;
|
|
119
|
+
protected readonly panel: PopoverController;
|
|
73
120
|
/**
|
|
74
121
|
* The caret, held across a value the consumer wrote rather than the reader.
|
|
75
122
|
*
|
|
@@ -83,6 +130,40 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
83
130
|
private get tokens();
|
|
84
131
|
/** Whether there is a clear button on screen right now. */
|
|
85
132
|
private get showsClear();
|
|
133
|
+
/**
|
|
134
|
+
* The suggestions, in the order a reader moves through them.
|
|
135
|
+
*
|
|
136
|
+
* **Groups are opened out rather than walked into.** A slot assigns the group
|
|
137
|
+
* element, not the options inside it, so a panel that read only what was
|
|
138
|
+
* assigned would offer nothing at all the moment a consumer reached for
|
|
139
|
+
* `option-group` to say what a run of values belongs to.
|
|
140
|
+
*
|
|
141
|
+
* A disabled option keeps its place on screen and is skipped here, which is
|
|
142
|
+
* the difference between a list that explains why something cannot be chosen
|
|
143
|
+
* and one that pretends it was never there.
|
|
144
|
+
*/
|
|
145
|
+
private get suggestions();
|
|
146
|
+
/**
|
|
147
|
+
* The caret and the selection, under the platform's own names.
|
|
148
|
+
*
|
|
149
|
+
* **Published for the reason `Input` publishes them, and this element needs
|
|
150
|
+
* them more.** The `<input>` is inside this shadow root, so a consumer has no
|
|
151
|
+
* way to ask where the caret is: `querySelector` does not cross the boundary.
|
|
152
|
+
* A grammar that completes what is being typed cannot work without it, because
|
|
153
|
+
* "what is being typed" is the term the caret is in, and after writing a
|
|
154
|
+
* completion back the caret has to be put after it or the next parse reads the
|
|
155
|
+
* wrong term.
|
|
156
|
+
*
|
|
157
|
+
* Zero before there is a control, which is where an empty field's caret would
|
|
158
|
+
* be anyway.
|
|
159
|
+
*/
|
|
160
|
+
get selectionDirection(): 'backward' | 'forward' | 'none';
|
|
161
|
+
/** Where the selection ends, which is the caret when nothing is selected. */
|
|
162
|
+
get selectionEnd(): number;
|
|
163
|
+
/** Where the selection starts, which is the caret when nothing is selected. */
|
|
164
|
+
get selectionStart(): number;
|
|
165
|
+
/** Moves the caret, or selects a run of the text. */
|
|
166
|
+
setSelectionRange(start: number, end: number, direction?: 'backward' | 'forward' | 'none'): void;
|
|
86
167
|
/**
|
|
87
168
|
* Every keystroke, cancelable, and put back where it is refused.
|
|
88
169
|
*
|
|
@@ -100,7 +181,52 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
100
181
|
* disappear, which would otherwise land on the body.
|
|
101
182
|
*/
|
|
102
183
|
private readonly handleClear;
|
|
184
|
+
/**
|
|
185
|
+
* Opens the panel when there is something in it, and closes it when there is
|
|
186
|
+
* not.
|
|
187
|
+
*
|
|
188
|
+
* **Derived rather than commanded, which is the whole of how two levels
|
|
189
|
+
* work.** A reader who picks `disposition` wants its values next, and the
|
|
190
|
+
* consumer answers by rendering those values; if picking closed the panel it
|
|
191
|
+
* would shut and reopen between two renders, which a reader sees as a flicker
|
|
192
|
+
* and a screen reader hears as a list going away. Nothing here closes on a
|
|
193
|
+
* choice: the panel goes when the consumer stops offering, which is exactly
|
|
194
|
+
* when a value has been completed and there is nothing left to say.
|
|
195
|
+
*
|
|
196
|
+
* Through `requestOpen` for both directions, so `helix-before-open` and
|
|
197
|
+
* `helix-before-close` still gate it and a consumer who refuses is obeyed.
|
|
198
|
+
* **No guard of its own**, because that method already returns early when the
|
|
199
|
+
* answer has not moved: this runs after every render, and a second guard here
|
|
200
|
+
* would only be a copy of that one going stale.
|
|
201
|
+
*/
|
|
202
|
+
private settleOpen;
|
|
203
|
+
/** Moves the highlight, wrapping, and opens the panel if it was closed. */
|
|
204
|
+
private move;
|
|
103
205
|
private readonly handleKeydown;
|
|
206
|
+
/**
|
|
207
|
+
* What is on offer, settled before the render that draws it.
|
|
208
|
+
*
|
|
209
|
+
* **Not left to `slotchange`, for the reason the token row is not.** That
|
|
210
|
+
* event reports the assignment moving and nothing else, and it does not fire
|
|
211
|
+
* at all under the harness these are tested in, so a panel that learned only
|
|
212
|
+
* from it would be a panel that never opened. Reading the slot every update is
|
|
213
|
+
* what makes the answer true whenever anyone asks.
|
|
214
|
+
*
|
|
215
|
+
* State only. Opening is an announcement and belongs after the render, which
|
|
216
|
+
* is why `settleOpen` is not called from here.
|
|
217
|
+
*/
|
|
218
|
+
private refreshSuggesting;
|
|
219
|
+
/** The panel again, whenever what is in it changes. */
|
|
220
|
+
private readonly handleSuggestions;
|
|
221
|
+
/** Focus arriving in the field is a reader asking what they can type. */
|
|
222
|
+
private readonly handleFocusIn;
|
|
223
|
+
/**
|
|
224
|
+
* Focus leaving the element closes it.
|
|
225
|
+
*
|
|
226
|
+
* `relatedTarget` and not a timer: a press inside the panel moves focus within
|
|
227
|
+
* this element and must not close what is being pressed.
|
|
228
|
+
*/
|
|
229
|
+
private readonly handleFocusOut;
|
|
104
230
|
/** The row again, whenever what is in it changes. */
|
|
105
231
|
private readonly handleTokens;
|
|
106
232
|
/**
|
|
@@ -117,6 +243,23 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
117
243
|
protected willUpdate(changed: PropertyValues): void;
|
|
118
244
|
protected firstUpdated(changed: PropertyValues): void;
|
|
119
245
|
protected updated(changed: PropertyValues): void;
|
|
246
|
+
/**
|
|
247
|
+
* Marks the highlighted suggestion, and tells the control which one it is.
|
|
248
|
+
*
|
|
249
|
+
* **Two halves, and neither does the other's job.** `active` is the option's
|
|
250
|
+
* own published flag and is what a stylesheet draws from. The control carries
|
|
251
|
+
* `aria-activedescendant`, because focus never leaves the text: a screen
|
|
252
|
+
* reader is told what is highlighted by the field the caret is in, not by the
|
|
253
|
+
* list.
|
|
254
|
+
*
|
|
255
|
+
* Through `ariaActiveDescendantElement` rather than an id, which is what
|
|
256
|
+
* `Listbox` reaches for and for the same two reasons: it crosses a shadow
|
|
257
|
+
* boundary, where an `aria-activedescendant` naming an id cannot, and it needs
|
|
258
|
+
* no ids invented for the consumer's own elements. Feature-tested, because
|
|
259
|
+
* where it is missing the assignment is a silent no-op rather than an error,
|
|
260
|
+
* and the highlight is still drawn.
|
|
261
|
+
*/
|
|
262
|
+
private markActive;
|
|
120
263
|
/** Puts the caret in the field, the way a label pointing at one would. */
|
|
121
264
|
focus(options?: FocusOptions): void;
|
|
122
265
|
protected render(): TemplateResult;
|
|
@@ -4,12 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { AriaForwardController, customElement, drawIcon, HelixFormElement, holdCaret, MixinControllerValue, MixinField, MixinTokens, restoreValue, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, customElement, drawIcon, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, restoreValue, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { x } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
const asksToGo = (node) => typeof node?.dismiss === 'function';
|
|
12
|
-
|
|
12
|
+
const isOption = (node) => 'active' in node && 'value' in node;
|
|
13
|
+
let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
|
|
13
14
|
constructor() {
|
|
14
15
|
super(...arguments);
|
|
15
16
|
this.aria = new AriaForwardController(this, {
|
|
@@ -20,11 +21,27 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
|
|
|
20
21
|
this.clearLabel = 'Clear';
|
|
21
22
|
this.placeholder = '';
|
|
22
23
|
this.over = 0;
|
|
24
|
+
this.activeAt = -1;
|
|
25
|
+
this.suggesting = false;
|
|
26
|
+
this.dismissed = false;
|
|
27
|
+
this.panel = new PopoverController(this, {
|
|
28
|
+
anchor: () => this.renderRoot?.querySelector('[part="field"]'),
|
|
29
|
+
boundary: () => [
|
|
30
|
+
this.renderRoot?.querySelector('[part="surface"]'),
|
|
31
|
+
this.renderRoot?.querySelector('[part="field"]'),
|
|
32
|
+
],
|
|
33
|
+
offset: () => 4,
|
|
34
|
+
onDismiss: () => this.requestOpen(false),
|
|
35
|
+
placement: 'bottom-start',
|
|
36
|
+
surface: () => this.renderRoot?.querySelector('[part="surface"]'),
|
|
37
|
+
});
|
|
23
38
|
this.handleInput = (event) => {
|
|
24
39
|
event.stopPropagation();
|
|
25
40
|
const control = event.target;
|
|
26
41
|
this.requestValue(control.value);
|
|
27
42
|
restoreValue(control, this.value);
|
|
43
|
+
this.dismissed = false;
|
|
44
|
+
this.settleOpen();
|
|
28
45
|
};
|
|
29
46
|
this.handleChange = (event) => {
|
|
30
47
|
event.stopPropagation();
|
|
@@ -39,7 +56,68 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
|
|
|
39
56
|
}
|
|
40
57
|
};
|
|
41
58
|
this.handleKeydown = (event) => {
|
|
42
|
-
this.tokenKeydown(event, this.value)
|
|
59
|
+
if (this.tokenKeydown(event, this.value)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!this.suggesting) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
switch (event.key) {
|
|
66
|
+
case 'ArrowDown':
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
this.move(1);
|
|
69
|
+
return;
|
|
70
|
+
case 'ArrowUp':
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
this.move(-1);
|
|
73
|
+
return;
|
|
74
|
+
case 'Home':
|
|
75
|
+
if (this.open) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
this.activeAt = 0;
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
case 'End':
|
|
81
|
+
if (this.open) {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
this.activeAt = this.suggestions.length - 1;
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
case 'Escape':
|
|
87
|
+
if (this.open) {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
event.stopPropagation();
|
|
90
|
+
this.dismissed = true;
|
|
91
|
+
this.activeAt = -1;
|
|
92
|
+
this.requestOpen(false);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
case 'Enter': {
|
|
96
|
+
const active = this.suggestions[this.activeAt];
|
|
97
|
+
if (!this.open || !active) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
active.select();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
default:
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
this.handleSuggestions = () => {
|
|
108
|
+
this.refreshSuggesting();
|
|
109
|
+
this.settleOpen();
|
|
110
|
+
};
|
|
111
|
+
this.handleFocusIn = () => {
|
|
112
|
+
this.settleOpen();
|
|
113
|
+
};
|
|
114
|
+
this.handleFocusOut = (event) => {
|
|
115
|
+
const next = event.relatedTarget;
|
|
116
|
+
if (next instanceof Node && this.contains(next)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.activeAt = -1;
|
|
120
|
+
this.requestOpen(false);
|
|
43
121
|
};
|
|
44
122
|
this.handleTokens = () => {
|
|
45
123
|
this.over = this.hideOverflow(this.tokens);
|
|
@@ -58,6 +136,56 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
|
|
|
58
136
|
get showsClear() {
|
|
59
137
|
return this.clearable && this.value !== '' && !this.isDisabled;
|
|
60
138
|
}
|
|
139
|
+
get suggestions() {
|
|
140
|
+
const slot = this.renderRoot?.querySelector('slot[name="suggestions"]');
|
|
141
|
+
const found = [];
|
|
142
|
+
for (const node of slot?.assignedElements({ flatten: true }) ?? []) {
|
|
143
|
+
if (isOption(node)) {
|
|
144
|
+
found.push(node);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
for (const child of node.children) {
|
|
148
|
+
if (isOption(child)) {
|
|
149
|
+
found.push(child);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return found.filter((option) => !option.disabled);
|
|
154
|
+
}
|
|
155
|
+
get selectionDirection() {
|
|
156
|
+
return this.control?.selectionDirection ?? 'none';
|
|
157
|
+
}
|
|
158
|
+
get selectionEnd() {
|
|
159
|
+
return this.control?.selectionEnd ?? 0;
|
|
160
|
+
}
|
|
161
|
+
get selectionStart() {
|
|
162
|
+
return this.control?.selectionStart ?? 0;
|
|
163
|
+
}
|
|
164
|
+
setSelectionRange(start, end, direction) {
|
|
165
|
+
this.control?.setSelectionRange(start, end, direction);
|
|
166
|
+
}
|
|
167
|
+
settleOpen() {
|
|
168
|
+
const focused = this.matches(':focus-within') || this.contains(document.activeElement);
|
|
169
|
+
this.requestOpen(this.suggesting && !this.dismissed && !this.isDisabled && focused);
|
|
170
|
+
}
|
|
171
|
+
move(by) {
|
|
172
|
+
const options = this.suggestions;
|
|
173
|
+
if (options.length === 0) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!this.open) {
|
|
177
|
+
this.dismissed = false;
|
|
178
|
+
this.settleOpen();
|
|
179
|
+
}
|
|
180
|
+
const from = this.activeAt;
|
|
181
|
+
this.activeAt =
|
|
182
|
+
from < 0 ? (by > 0 ? 0 : options.length - 1) : (from + by + options.length) % options.length;
|
|
183
|
+
}
|
|
184
|
+
refreshSuggesting() {
|
|
185
|
+
const options = this.suggestions;
|
|
186
|
+
this.suggesting = options.length > 0;
|
|
187
|
+
this.activeAt = this.activeAt < options.length ? this.activeAt : -1;
|
|
188
|
+
}
|
|
61
189
|
removeLastToken() {
|
|
62
190
|
const last = this.tokens?.assignedElements({ flatten: true }).at(-1);
|
|
63
191
|
if (asksToGo(last)) {
|
|
@@ -74,23 +202,46 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
|
|
|
74
202
|
this.releaseCaret = holdCaret(control);
|
|
75
203
|
}
|
|
76
204
|
this.handleTokens();
|
|
205
|
+
this.refreshSuggesting();
|
|
77
206
|
}
|
|
78
207
|
firstUpdated(changed) {
|
|
79
208
|
super.firstUpdated(changed);
|
|
80
209
|
this.handleTokens();
|
|
210
|
+
this.refreshSuggesting();
|
|
81
211
|
}
|
|
82
212
|
updated(changed) {
|
|
83
213
|
super.updated(changed);
|
|
84
214
|
this.releaseCaret?.();
|
|
85
215
|
this.releaseCaret = undefined;
|
|
86
216
|
this.internals.setFormValue(this.value);
|
|
217
|
+
this.markActive();
|
|
218
|
+
if (this.open) {
|
|
219
|
+
this.panel.show();
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this.panel.hide();
|
|
223
|
+
}
|
|
224
|
+
this.settleOpen();
|
|
225
|
+
}
|
|
226
|
+
markActive() {
|
|
227
|
+
const options = this.suggestions;
|
|
228
|
+
const active = this.open ? options[this.activeAt] : undefined;
|
|
229
|
+
for (const [at, option] of options.entries()) {
|
|
230
|
+
option.active = this.open && at === this.activeAt;
|
|
231
|
+
}
|
|
232
|
+
const control = this.control;
|
|
233
|
+
if (control && 'ariaActiveDescendantElement' in control) {
|
|
234
|
+
;
|
|
235
|
+
control.ariaActiveDescendantElement = active ?? null;
|
|
236
|
+
}
|
|
237
|
+
active?.scrollIntoView({ block: 'nearest' });
|
|
87
238
|
}
|
|
88
239
|
focus(options) {
|
|
89
240
|
this.control?.focus(options);
|
|
90
241
|
}
|
|
91
242
|
render() {
|
|
92
243
|
return html `${this.renderLabel()}
|
|
93
|
-
<div part="field">
|
|
244
|
+
<div part="field" @focusin=${this.handleFocusIn} @focusout=${this.handleFocusOut}>
|
|
94
245
|
<slot name="start"></slot>
|
|
95
246
|
${this.renderTokenRow(html `<slot name="tags" @slotchange=${this.handleTokens}></slot>`, this.over)}
|
|
96
247
|
<input
|
|
@@ -101,6 +252,10 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
|
|
|
101
252
|
name=${this.name || nothing}
|
|
102
253
|
placeholder=${this.placeholder || nothing}
|
|
103
254
|
aria-describedby=${this.describedBy ?? nothing}
|
|
255
|
+
role=${this.suggesting ? 'combobox' : nothing}
|
|
256
|
+
aria-autocomplete=${this.suggesting ? 'list' : nothing}
|
|
257
|
+
aria-controls=${this.suggesting ? 'suggestions' : nothing}
|
|
258
|
+
aria-expanded=${this.suggesting ? (this.open ? 'true' : 'false') : nothing}
|
|
104
259
|
?disabled=${this.isDisabled}
|
|
105
260
|
?required=${this.required}
|
|
106
261
|
@input=${this.handleInput}
|
|
@@ -119,6 +274,14 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
|
|
|
119
274
|
</button>`
|
|
120
275
|
: nothing}
|
|
121
276
|
</div>
|
|
277
|
+
<!-- **Always rendered, and hidden rather than absent**, for the reason the
|
|
278
|
+
token row is: a slot that is not in the tree assigns nothing, so a panel
|
|
279
|
+
built only once there were options could never learn that there are
|
|
280
|
+
any. A manual popover puts it in the top layer, so it is not clipped by
|
|
281
|
+
a scrolling table or an overflow on an ancestor. -->
|
|
282
|
+
<div part="surface" id="suggestions" role="listbox" popover="manual" ?hidden=${!this.open}>
|
|
283
|
+
<slot name="suggestions" @slotchange=${this.handleSuggestions}></slot>
|
|
284
|
+
</div>
|
|
122
285
|
${this.renderHint()}${this.renderFeedback()}`;
|
|
123
286
|
}
|
|
124
287
|
};
|
|
@@ -134,6 +297,12 @@ __decorate([
|
|
|
134
297
|
__decorate([
|
|
135
298
|
state()
|
|
136
299
|
], HelixHeadlessFilterInput.prototype, "over", void 0);
|
|
300
|
+
__decorate([
|
|
301
|
+
state()
|
|
302
|
+
], HelixHeadlessFilterInput.prototype, "activeAt", void 0);
|
|
303
|
+
__decorate([
|
|
304
|
+
state()
|
|
305
|
+
], HelixHeadlessFilterInput.prototype, "suggesting", void 0);
|
|
137
306
|
HelixHeadlessFilterInput = __decorate([
|
|
138
307
|
customElement('helix-headless-filter-input')
|
|
139
308
|
], HelixHeadlessFilterInput);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-headless",
|
|
3
3
|
"description": "Headless component tier. Structure, accessibility, state, and form participation. No styling.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
},
|
|
46
46
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@fusion.dev/helix-core": "0.
|
|
49
|
-
"@fusion.dev/helix-icons": "0.
|
|
48
|
+
"@fusion.dev/helix-core": "0.15.0",
|
|
49
|
+
"@fusion.dev/helix-icons": "0.15.0",
|
|
50
50
|
"uqr": "^0.1.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@custom-elements-manifest/analyzer": "^0.11.0",
|
|
54
|
-
"@fusion.dev/helix-cem": "0.
|
|
55
|
-
"@fusion.dev/helix-testing": "0.
|
|
54
|
+
"@fusion.dev/helix-cem": "0.15.0",
|
|
55
|
+
"@fusion.dev/helix-testing": "0.15.0",
|
|
56
56
|
"@fusion.dev/scripts": "0.0.0-dev",
|
|
57
57
|
"@happy-dom/global-registrator": "^20.0.0",
|
|
58
58
|
"@lit-labs/ssr": "^3.3.0",
|
package/types/jsx.d.ts
CHANGED
|
@@ -5800,6 +5800,12 @@ export type HelixHeadlessFilterInputElementEvent<E = Event> = TypedEvent<
|
|
|
5800
5800
|
/** `helix-before-change` event type */
|
|
5801
5801
|
export type HelixHeadlessFilterInputHelixBeforeChangeElementEvent =
|
|
5802
5802
|
HelixHeadlessFilterInputElementEvent<HelixBeforeChangeEvent>;
|
|
5803
|
+
/** `helix-before-close` event type */
|
|
5804
|
+
export type HelixHeadlessFilterInputHelixBeforeCloseElementEvent =
|
|
5805
|
+
HelixHeadlessFilterInputElementEvent<HelixBeforeCloseEvent>;
|
|
5806
|
+
/** `helix-before-open` event type */
|
|
5807
|
+
export type HelixHeadlessFilterInputHelixBeforeOpenElementEvent =
|
|
5808
|
+
HelixHeadlessFilterInputElementEvent<HelixBeforeOpenEvent>;
|
|
5803
5809
|
|
|
5804
5810
|
export type HelixHeadlessFilterInputProps = {
|
|
5805
5811
|
/** Whether the field offers a button that empties it.
|
|
@@ -5817,6 +5823,23 @@ them. */
|
|
|
5817
5823
|
|
|
5818
5824
|
Not a label: it goes when typing starts. */
|
|
5819
5825
|
placeholder?: (HelixHeadlessFilterInput["placeholder"]) | undefined;
|
|
5826
|
+
/** Whether the element is open.
|
|
5827
|
+
|
|
5828
|
+
One property, not two, so `dialog.open` answers the question anyone
|
|
5829
|
+
asking it meant. Writing always applies; `<helix-dialog open>` is the
|
|
5830
|
+
same statement in markup.
|
|
5831
|
+
|
|
5832
|
+
**Not reflected, and that is required rather than tidy.** Reflection
|
|
5833
|
+
writes the resolved state back as an attribute, so an element that opened
|
|
5834
|
+
itself would put `open` on its own host: cloned, serialized for
|
|
5835
|
+
hydration, or diffed by a framework, it comes back holding whatever the
|
|
5836
|
+
last reader did rather than what the author wrote. `:state(open)` carries
|
|
5837
|
+
the resolved state for stylesheets, which is what the mirror is for. */
|
|
5838
|
+
open?: (HelixHeadlessFilterInput["open"]) | undefined;
|
|
5839
|
+
/** Initial open state for uncontrolled use, where nothing declared one. */
|
|
5840
|
+
"default-open"?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
|
|
5841
|
+
/** Initial open state for uncontrolled use, where nothing declared one. */
|
|
5842
|
+
defaultOpen?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
|
|
5820
5843
|
/** What the control says once everything is showing. */
|
|
5821
5844
|
"collapse-label"?: (HelixHeadlessFilterInput["collapseLabel"]) | undefined;
|
|
5822
5845
|
/** What the control says once everything is showing. */
|
|
@@ -5883,8 +5906,18 @@ Here rather than on each control. */
|
|
|
5883
5906
|
"onhelix-before-change"?: ((
|
|
5884
5907
|
e: HelixHeadlessFilterInputHelixBeforeChangeElementEvent,
|
|
5885
5908
|
) => void) | undefined;
|
|
5909
|
+
/** Fired before the suggestions go, and cancelable. */
|
|
5910
|
+
"onhelix-before-close"?: ((
|
|
5911
|
+
e: HelixHeadlessFilterInputHelixBeforeCloseElementEvent,
|
|
5912
|
+
) => void) | undefined;
|
|
5913
|
+
/** Fired before the suggestions show, and cancelable. */
|
|
5914
|
+
"onhelix-before-open"?: ((
|
|
5915
|
+
e: HelixHeadlessFilterInputHelixBeforeOpenElementEvent,
|
|
5916
|
+
) => void) | undefined;
|
|
5886
5917
|
/** Announced once the text has moved. */
|
|
5887
5918
|
oninput?: ((e: HelixHeadlessFilterInputElementEvent) => void) | undefined;
|
|
5919
|
+
/** Says the panel moved, carrying `newState` and `oldState`. */
|
|
5920
|
+
ontoggle?: ((e: HelixHeadlessFilterInputElementEvent) => void) | undefined;
|
|
5888
5921
|
};
|
|
5889
5922
|
|
|
5890
5923
|
export type HelixHeadlessFilterInputSolidJsProps = {
|
|
@@ -5903,6 +5936,23 @@ them. */
|
|
|
5903
5936
|
|
|
5904
5937
|
Not a label: it goes when typing starts. */
|
|
5905
5938
|
"prop:placeholder"?: (HelixHeadlessFilterInput["placeholder"]) | undefined;
|
|
5939
|
+
/** Whether the element is open.
|
|
5940
|
+
|
|
5941
|
+
One property, not two, so `dialog.open` answers the question anyone
|
|
5942
|
+
asking it meant. Writing always applies; `<helix-dialog open>` is the
|
|
5943
|
+
same statement in markup.
|
|
5944
|
+
|
|
5945
|
+
**Not reflected, and that is required rather than tidy.** Reflection
|
|
5946
|
+
writes the resolved state back as an attribute, so an element that opened
|
|
5947
|
+
itself would put `open` on its own host: cloned, serialized for
|
|
5948
|
+
hydration, or diffed by a framework, it comes back holding whatever the
|
|
5949
|
+
last reader did rather than what the author wrote. `:state(open)` carries
|
|
5950
|
+
the resolved state for stylesheets, which is what the mirror is for. */
|
|
5951
|
+
"prop:open"?: (HelixHeadlessFilterInput["open"]) | undefined;
|
|
5952
|
+
/** Initial open state for uncontrolled use, where nothing declared one. */
|
|
5953
|
+
"bool:default-open"?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
|
|
5954
|
+
/** Initial open state for uncontrolled use, where nothing declared one. */
|
|
5955
|
+
"prop:defaultOpen"?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
|
|
5906
5956
|
/** What the control says once everything is showing. */
|
|
5907
5957
|
"attr:collapse-label"?: (HelixHeadlessFilterInput["collapseLabel"]) | undefined;
|
|
5908
5958
|
/** What the control says once everything is showing. */
|
|
@@ -5968,8 +6018,18 @@ Here rather than on each control. */
|
|
|
5968
6018
|
"on:helix-before-change"?: ((
|
|
5969
6019
|
e: HelixHeadlessFilterInputHelixBeforeChangeElementEvent,
|
|
5970
6020
|
) => void) | undefined;
|
|
6021
|
+
/** Fired before the suggestions go, and cancelable. */
|
|
6022
|
+
"on:helix-before-close"?: ((
|
|
6023
|
+
e: HelixHeadlessFilterInputHelixBeforeCloseElementEvent,
|
|
6024
|
+
) => void) | undefined;
|
|
6025
|
+
/** Fired before the suggestions show, and cancelable. */
|
|
6026
|
+
"on:helix-before-open"?: ((
|
|
6027
|
+
e: HelixHeadlessFilterInputHelixBeforeOpenElementEvent,
|
|
6028
|
+
) => void) | undefined;
|
|
5971
6029
|
/** Announced once the text has moved. */
|
|
5972
6030
|
"on:input"?: ((e: HelixHeadlessFilterInputElementEvent) => void) | undefined;
|
|
6031
|
+
/** Says the panel moved, carrying `newState` and `oldState`. */
|
|
6032
|
+
"on:toggle"?: ((e: HelixHeadlessFilterInputElementEvent) => void) | undefined;
|
|
5973
6033
|
|
|
5974
6034
|
/** Set the innerHTML of the element */
|
|
5975
6035
|
innerHTML?: (string) | undefined;
|
|
@@ -20567,6 +20627,20 @@ export type CustomElements = {
|
|
|
20567
20627
|
* `helix-dismiss` its own remove control fires, so a consumer who handled the
|
|
20568
20628
|
* pointer has already handled the keyboard.
|
|
20569
20629
|
*
|
|
20630
|
+
* **It can suggest what to type next, and it never learns the grammar.** Slot
|
|
20631
|
+
* `helix-option` elements into `suggestions` and the field shows them, moves
|
|
20632
|
+
* through them with the arrows, and lets Enter press one; the option fires its
|
|
20633
|
+
* own `helix-select` and the consumer decides what choosing meant. Deciding
|
|
20634
|
+
* *what* to suggest is theirs too: keys before a colon and values after it are
|
|
20635
|
+
* the same panel with different contents, which is all a two-level filter is,
|
|
20636
|
+
* and the parse that tells them apart belongs where the domain does.
|
|
20637
|
+
*
|
|
20638
|
+
* **The panel opens when there is something in it and closes when there is
|
|
20639
|
+
* not**, rather than on a choice. A reader who picks a key wants its values
|
|
20640
|
+
* next, and closing on the choice would shut the panel between two renders. Use
|
|
20641
|
+
* `selectionStart` and `setSelectionRange` to write a completion back and put
|
|
20642
|
+
* the caret after it, or the next parse reads the wrong term.
|
|
20643
|
+
*
|
|
20570
20644
|
* ## Attributes & Properties
|
|
20571
20645
|
*
|
|
20572
20646
|
* Component attributes and properties that can be applied to the element or by using JavaScript.
|
|
@@ -20581,6 +20655,19 @@ export type CustomElements = {
|
|
|
20581
20655
|
* - `placeholder`: Shown while the field is empty.
|
|
20582
20656
|
*
|
|
20583
20657
|
* Not a label: it goes when typing starts.
|
|
20658
|
+
* - `open`: Whether the element is open.
|
|
20659
|
+
*
|
|
20660
|
+
* One property, not two, so `dialog.open` answers the question anyone
|
|
20661
|
+
* asking it meant. Writing always applies; `<helix-dialog open>` is the
|
|
20662
|
+
* same statement in markup.
|
|
20663
|
+
*
|
|
20664
|
+
* **Not reflected, and that is required rather than tidy.** Reflection
|
|
20665
|
+
* writes the resolved state back as an attribute, so an element that opened
|
|
20666
|
+
* itself would put `open` on its own host: cloned, serialized for
|
|
20667
|
+
* hydration, or diffed by a framework, it comes back holding whatever the
|
|
20668
|
+
* last reader did rather than what the author wrote. `:state(open)` carries
|
|
20669
|
+
* the resolved state for stylesheets, which is what the mirror is for.
|
|
20670
|
+
* - `default-open`/`defaultOpen`: Initial open state for uncontrolled use, where nothing declared one.
|
|
20584
20671
|
* - `collapse-label`/`collapseLabel`: What the control says once everything is showing.
|
|
20585
20672
|
* - `expandable`: Whether the count is a control that shows the rest.
|
|
20586
20673
|
* - `expand-label`/`expandLabel`: What the collapsed count is called, since `+3` names nothing.
|
|
@@ -20620,6 +20707,20 @@ export type CustomElements = {
|
|
|
20620
20707
|
* imperatively later, and nothing is hydrating by then.
|
|
20621
20708
|
*
|
|
20622
20709
|
* Here rather than on each control.
|
|
20710
|
+
* - `selectionDirection`: The caret and the selection, under the platform's own names.
|
|
20711
|
+
*
|
|
20712
|
+
* **Published for the reason `Input` publishes them, and this element needs
|
|
20713
|
+
* them more.** The `<input>` is inside this shadow root, so a consumer has no
|
|
20714
|
+
* way to ask where the caret is: `querySelector` does not cross the boundary.
|
|
20715
|
+
* A grammar that completes what is being typed cannot work without it, because
|
|
20716
|
+
* "what is being typed" is the term the caret is in, and after writing a
|
|
20717
|
+
* completion back the caret has to be put after it or the next parse reads the
|
|
20718
|
+
* wrong term.
|
|
20719
|
+
*
|
|
20720
|
+
* Zero before there is a control, which is where an empty field's caret would
|
|
20721
|
+
* be anyway. (property only) (readonly)
|
|
20722
|
+
* - `selectionEnd`: Where the selection ends, which is the caret when nothing is selected. (property only) (readonly)
|
|
20723
|
+
* - `selectionStart`: Where the selection starts, which is the caret when nothing is selected. (property only) (readonly)
|
|
20623
20724
|
* - `form`: The form this control belongs to, if any. (property only) (readonly)
|
|
20624
20725
|
* - `labels`: Labels associated with this control. (property only) (readonly)
|
|
20625
20726
|
* - `whenSettled`: Resolves a frame after the first update, when transitions are switched on.
|
|
@@ -20647,7 +20748,10 @@ export type CustomElements = {
|
|
|
20647
20748
|
*
|
|
20648
20749
|
* - `change`: Relayed from the input once the reader has finished.
|
|
20649
20750
|
* - `helix-before-change`: Fired as the text changes, and cancelable.
|
|
20751
|
+
* - `helix-before-close`: Fired before the suggestions go, and cancelable.
|
|
20752
|
+
* - `helix-before-open`: Fired before the suggestions show, and cancelable.
|
|
20650
20753
|
* - `input`: Announced once the text has moved.
|
|
20754
|
+
* - `toggle`: Says the panel moved, carrying `newState` and `oldState`.
|
|
20651
20755
|
*
|
|
20652
20756
|
* ## Slots
|
|
20653
20757
|
*
|
|
@@ -20655,13 +20759,23 @@ export type CustomElements = {
|
|
|
20655
20759
|
*
|
|
20656
20760
|
* - `end`: After the text, for a count or a control.
|
|
20657
20761
|
* - `start`: Before the text, for a search icon.
|
|
20762
|
+
* - `suggestions`: What could be typed next, as `Option`s, and `Option.Group`s of them.
|
|
20658
20763
|
* - `tags`: What is already narrowing the results, as `Tag`s.
|
|
20659
20764
|
*
|
|
20660
20765
|
* ## Methods
|
|
20661
20766
|
*
|
|
20662
20767
|
* Methods that can be called to access component functionality.
|
|
20663
20768
|
*
|
|
20769
|
+
* - `setSelectionRange(start: number, end: number, direction?: 'backward' | 'forward' | 'none') => void`: Moves the caret, or selects a run of the text.
|
|
20664
20770
|
* - `focus(options?: FocusOptions) => void`: Puts the caret in the field, the way a label pointing at one would.
|
|
20771
|
+
* - `requestOpen(next: boolean) => boolean`: Requests an open-state change, and reports whether it was applied.
|
|
20772
|
+
*
|
|
20773
|
+
* Announces before it acts, so `preventDefault()` on the `helix-before-open` or
|
|
20774
|
+
* `helix-before-close` is a refusal and nothing moves. Nobody refusing means the
|
|
20775
|
+
* element opens or closes itself, whether or not a consumer declared
|
|
20776
|
+
* `open`: a declared property is a seed and a correction, and a consumer
|
|
20777
|
+
* who wants the decision for themselves refuses it and assigns their own.
|
|
20778
|
+
* - `toggleOpen() => boolean`: Requests the opposite of the current state.
|
|
20665
20779
|
* - `requestValue(next: string) => boolean`: Requests a new value, and reports whether the request stood.
|
|
20666
20780
|
*
|
|
20667
20781
|
* Announces before it acts, so `preventDefault()` on the `helix-before-change` is
|
|
@@ -20724,6 +20838,7 @@ export type CustomElements = {
|
|
|
20724
20838
|
* - `hint`: Help under the field, while nothing is wrong.
|
|
20725
20839
|
* - `label`: The question, over the field.
|
|
20726
20840
|
* - `rest`: How many tokens did not fit. A `<button>` with `expandable`, a `<span>` without it.
|
|
20841
|
+
* - `surface`: The floating panel the suggestions sit in.
|
|
20727
20842
|
* - `tags`: The row the tokens sit in.
|
|
20728
20843
|
*/
|
|
20729
20844
|
"helix-headless-filter-input": Partial<
|
|
@@ -35007,6 +35122,20 @@ export type CustomElementsSolidJs = {
|
|
|
35007
35122
|
* `helix-dismiss` its own remove control fires, so a consumer who handled the
|
|
35008
35123
|
* pointer has already handled the keyboard.
|
|
35009
35124
|
*
|
|
35125
|
+
* **It can suggest what to type next, and it never learns the grammar.** Slot
|
|
35126
|
+
* `helix-option` elements into `suggestions` and the field shows them, moves
|
|
35127
|
+
* through them with the arrows, and lets Enter press one; the option fires its
|
|
35128
|
+
* own `helix-select` and the consumer decides what choosing meant. Deciding
|
|
35129
|
+
* *what* to suggest is theirs too: keys before a colon and values after it are
|
|
35130
|
+
* the same panel with different contents, which is all a two-level filter is,
|
|
35131
|
+
* and the parse that tells them apart belongs where the domain does.
|
|
35132
|
+
*
|
|
35133
|
+
* **The panel opens when there is something in it and closes when there is
|
|
35134
|
+
* not**, rather than on a choice. A reader who picks a key wants its values
|
|
35135
|
+
* next, and closing on the choice would shut the panel between two renders. Use
|
|
35136
|
+
* `selectionStart` and `setSelectionRange` to write a completion back and put
|
|
35137
|
+
* the caret after it, or the next parse reads the wrong term.
|
|
35138
|
+
*
|
|
35010
35139
|
* ## Attributes & Properties
|
|
35011
35140
|
*
|
|
35012
35141
|
* Component attributes and properties that can be applied to the element or by using JavaScript.
|
|
@@ -35021,6 +35150,19 @@ export type CustomElementsSolidJs = {
|
|
|
35021
35150
|
* - `placeholder`: Shown while the field is empty.
|
|
35022
35151
|
*
|
|
35023
35152
|
* Not a label: it goes when typing starts.
|
|
35153
|
+
* - `open`: Whether the element is open.
|
|
35154
|
+
*
|
|
35155
|
+
* One property, not two, so `dialog.open` answers the question anyone
|
|
35156
|
+
* asking it meant. Writing always applies; `<helix-dialog open>` is the
|
|
35157
|
+
* same statement in markup.
|
|
35158
|
+
*
|
|
35159
|
+
* **Not reflected, and that is required rather than tidy.** Reflection
|
|
35160
|
+
* writes the resolved state back as an attribute, so an element that opened
|
|
35161
|
+
* itself would put `open` on its own host: cloned, serialized for
|
|
35162
|
+
* hydration, or diffed by a framework, it comes back holding whatever the
|
|
35163
|
+
* last reader did rather than what the author wrote. `:state(open)` carries
|
|
35164
|
+
* the resolved state for stylesheets, which is what the mirror is for.
|
|
35165
|
+
* - `default-open`/`defaultOpen`: Initial open state for uncontrolled use, where nothing declared one.
|
|
35024
35166
|
* - `collapse-label`/`collapseLabel`: What the control says once everything is showing.
|
|
35025
35167
|
* - `expandable`: Whether the count is a control that shows the rest.
|
|
35026
35168
|
* - `expand-label`/`expandLabel`: What the collapsed count is called, since `+3` names nothing.
|
|
@@ -35060,6 +35202,20 @@ export type CustomElementsSolidJs = {
|
|
|
35060
35202
|
* imperatively later, and nothing is hydrating by then.
|
|
35061
35203
|
*
|
|
35062
35204
|
* Here rather than on each control.
|
|
35205
|
+
* - `selectionDirection`: The caret and the selection, under the platform's own names.
|
|
35206
|
+
*
|
|
35207
|
+
* **Published for the reason `Input` publishes them, and this element needs
|
|
35208
|
+
* them more.** The `<input>` is inside this shadow root, so a consumer has no
|
|
35209
|
+
* way to ask where the caret is: `querySelector` does not cross the boundary.
|
|
35210
|
+
* A grammar that completes what is being typed cannot work without it, because
|
|
35211
|
+
* "what is being typed" is the term the caret is in, and after writing a
|
|
35212
|
+
* completion back the caret has to be put after it or the next parse reads the
|
|
35213
|
+
* wrong term.
|
|
35214
|
+
*
|
|
35215
|
+
* Zero before there is a control, which is where an empty field's caret would
|
|
35216
|
+
* be anyway. (property only) (readonly)
|
|
35217
|
+
* - `selectionEnd`: Where the selection ends, which is the caret when nothing is selected. (property only) (readonly)
|
|
35218
|
+
* - `selectionStart`: Where the selection starts, which is the caret when nothing is selected. (property only) (readonly)
|
|
35063
35219
|
* - `form`: The form this control belongs to, if any. (property only) (readonly)
|
|
35064
35220
|
* - `labels`: Labels associated with this control. (property only) (readonly)
|
|
35065
35221
|
* - `whenSettled`: Resolves a frame after the first update, when transitions are switched on.
|
|
@@ -35087,7 +35243,10 @@ export type CustomElementsSolidJs = {
|
|
|
35087
35243
|
*
|
|
35088
35244
|
* - `change`: Relayed from the input once the reader has finished.
|
|
35089
35245
|
* - `helix-before-change`: Fired as the text changes, and cancelable.
|
|
35246
|
+
* - `helix-before-close`: Fired before the suggestions go, and cancelable.
|
|
35247
|
+
* - `helix-before-open`: Fired before the suggestions show, and cancelable.
|
|
35090
35248
|
* - `input`: Announced once the text has moved.
|
|
35249
|
+
* - `toggle`: Says the panel moved, carrying `newState` and `oldState`.
|
|
35091
35250
|
*
|
|
35092
35251
|
* ## Slots
|
|
35093
35252
|
*
|
|
@@ -35095,13 +35254,23 @@ export type CustomElementsSolidJs = {
|
|
|
35095
35254
|
*
|
|
35096
35255
|
* - `end`: After the text, for a count or a control.
|
|
35097
35256
|
* - `start`: Before the text, for a search icon.
|
|
35257
|
+
* - `suggestions`: What could be typed next, as `Option`s, and `Option.Group`s of them.
|
|
35098
35258
|
* - `tags`: What is already narrowing the results, as `Tag`s.
|
|
35099
35259
|
*
|
|
35100
35260
|
* ## Methods
|
|
35101
35261
|
*
|
|
35102
35262
|
* Methods that can be called to access component functionality.
|
|
35103
35263
|
*
|
|
35264
|
+
* - `setSelectionRange(start: number, end: number, direction?: 'backward' | 'forward' | 'none') => void`: Moves the caret, or selects a run of the text.
|
|
35104
35265
|
* - `focus(options?: FocusOptions) => void`: Puts the caret in the field, the way a label pointing at one would.
|
|
35266
|
+
* - `requestOpen(next: boolean) => boolean`: Requests an open-state change, and reports whether it was applied.
|
|
35267
|
+
*
|
|
35268
|
+
* Announces before it acts, so `preventDefault()` on the `helix-before-open` or
|
|
35269
|
+
* `helix-before-close` is a refusal and nothing moves. Nobody refusing means the
|
|
35270
|
+
* element opens or closes itself, whether or not a consumer declared
|
|
35271
|
+
* `open`: a declared property is a seed and a correction, and a consumer
|
|
35272
|
+
* who wants the decision for themselves refuses it and assigns their own.
|
|
35273
|
+
* - `toggleOpen() => boolean`: Requests the opposite of the current state.
|
|
35105
35274
|
* - `requestValue(next: string) => boolean`: Requests a new value, and reports whether the request stood.
|
|
35106
35275
|
*
|
|
35107
35276
|
* Announces before it acts, so `preventDefault()` on the `helix-before-change` is
|
|
@@ -35164,6 +35333,7 @@ export type CustomElementsSolidJs = {
|
|
|
35164
35333
|
* - `hint`: Help under the field, while nothing is wrong.
|
|
35165
35334
|
* - `label`: The question, over the field.
|
|
35166
35335
|
* - `rest`: How many tokens did not fit. A `<button>` with `expandable`, a `<span>` without it.
|
|
35336
|
+
* - `surface`: The floating panel the suggestions sit in.
|
|
35167
35337
|
* - `tags`: The row the tokens sit in.
|
|
35168
35338
|
*/
|
|
35169
35339
|
"helix-headless-filter-input": Partial<
|
package/types/vue.d.ts
CHANGED
|
@@ -2975,6 +2975,12 @@ export type HelixHeadlessFilterInputElementEvent<E = Event> = TypedEvent<
|
|
|
2975
2975
|
/** `helix-before-change` event type */
|
|
2976
2976
|
export type HelixHeadlessFilterInputHelixBeforeChangeElementEvent =
|
|
2977
2977
|
HelixHeadlessFilterInputElementEvent<HelixBeforeChangeEvent>;
|
|
2978
|
+
/** `helix-before-close` event type */
|
|
2979
|
+
export type HelixHeadlessFilterInputHelixBeforeCloseElementEvent =
|
|
2980
|
+
HelixHeadlessFilterInputElementEvent<HelixBeforeCloseEvent>;
|
|
2981
|
+
/** `helix-before-open` event type */
|
|
2982
|
+
export type HelixHeadlessFilterInputHelixBeforeOpenElementEvent =
|
|
2983
|
+
HelixHeadlessFilterInputElementEvent<HelixBeforeOpenEvent>;
|
|
2978
2984
|
|
|
2979
2985
|
export type HelixHeadlessFilterInputVueProps = {
|
|
2980
2986
|
/** Whether the field offers a button that empties it.
|
|
@@ -2992,6 +2998,23 @@ them. */
|
|
|
2992
2998
|
|
|
2993
2999
|
Not a label: it goes when typing starts. */
|
|
2994
3000
|
placeholder?: (HelixHeadlessFilterInput["placeholder"]) | undefined;
|
|
3001
|
+
/** Whether the element is open.
|
|
3002
|
+
|
|
3003
|
+
One property, not two, so `dialog.open` answers the question anyone
|
|
3004
|
+
asking it meant. Writing always applies; `<helix-dialog open>` is the
|
|
3005
|
+
same statement in markup.
|
|
3006
|
+
|
|
3007
|
+
**Not reflected, and that is required rather than tidy.** Reflection
|
|
3008
|
+
writes the resolved state back as an attribute, so an element that opened
|
|
3009
|
+
itself would put `open` on its own host: cloned, serialized for
|
|
3010
|
+
hydration, or diffed by a framework, it comes back holding whatever the
|
|
3011
|
+
last reader did rather than what the author wrote. `:state(open)` carries
|
|
3012
|
+
the resolved state for stylesheets, which is what the mirror is for. */
|
|
3013
|
+
open?: (HelixHeadlessFilterInput["open"]) | undefined;
|
|
3014
|
+
/** Initial open state for uncontrolled use, where nothing declared one. */
|
|
3015
|
+
"default-open"?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
|
|
3016
|
+
/** Initial open state for uncontrolled use, where nothing declared one. */
|
|
3017
|
+
defaultOpen?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
|
|
2995
3018
|
/** What the control says once everything is showing. */
|
|
2996
3019
|
"collapse-label"?: (HelixHeadlessFilterInput["collapseLabel"]) | undefined;
|
|
2997
3020
|
/** What the control says once everything is showing. */
|
|
@@ -3058,8 +3081,14 @@ export type HelixHeadlessFilterInputVueEvents = {
|
|
|
3058
3081
|
change: HelixHeadlessFilterInputElementEvent;
|
|
3059
3082
|
/** Fired as the text changes, and cancelable. */
|
|
3060
3083
|
"helix-before-change": HelixHeadlessFilterInputHelixBeforeChangeElementEvent;
|
|
3084
|
+
/** Fired before the suggestions go, and cancelable. */
|
|
3085
|
+
"helix-before-close": HelixHeadlessFilterInputHelixBeforeCloseElementEvent;
|
|
3086
|
+
/** Fired before the suggestions show, and cancelable. */
|
|
3087
|
+
"helix-before-open": HelixHeadlessFilterInputHelixBeforeOpenElementEvent;
|
|
3061
3088
|
/** Announced once the text has moved. */
|
|
3062
3089
|
input: HelixHeadlessFilterInputElementEvent;
|
|
3090
|
+
/** Says the panel moved, carrying `newState` and `oldState`. */
|
|
3091
|
+
toggle: HelixHeadlessFilterInputElementEvent;
|
|
3063
3092
|
};
|
|
3064
3093
|
|
|
3065
3094
|
export type HelixHeadlessFormatDateVueProps = {
|
|
@@ -8333,7 +8362,7 @@ export interface WcToolkitVueGlobalComponents {
|
|
|
8333
8362
|
HelixHeadlessFileInputVueProps,
|
|
8334
8363
|
HelixHeadlessFileInputVueEvents
|
|
8335
8364
|
>;
|
|
8336
|
-
/** **The tokens are the consumer's own elements, slotted.** What a filter narrows by belongs to the consumer's model rather than to this element: a facet with a prefix, a saved query, a range. So they are slotted rather than rendered from a list of strings, and removing one is a request this element makes rather than a change it applies. Combobox is the other way round, and that is the difference between the two: its tokens are a view of a selection it owns, drawn from options it was handed. **'value' is the text being typed, not the tokens**, which is what makes this a field a consumer can listen to. Every keystroke announces 'input' with the text on the element, so a table narrows as the reader types. The tokens need no reporting back, being markup the consumer already holds. Backspace on an empty field asks the last token to go, through the same 'helix-dismiss' its own remove control fires, so a consumer who handled the pointer has already handled the keyboard. */
|
|
8365
|
+
/** **The tokens are the consumer's own elements, slotted.** What a filter narrows by belongs to the consumer's model rather than to this element: a facet with a prefix, a saved query, a range. So they are slotted rather than rendered from a list of strings, and removing one is a request this element makes rather than a change it applies. Combobox is the other way round, and that is the difference between the two: its tokens are a view of a selection it owns, drawn from options it was handed. **'value' is the text being typed, not the tokens**, which is what makes this a field a consumer can listen to. Every keystroke announces 'input' with the text on the element, so a table narrows as the reader types. The tokens need no reporting back, being markup the consumer already holds. Backspace on an empty field asks the last token to go, through the same 'helix-dismiss' its own remove control fires, so a consumer who handled the pointer has already handled the keyboard. **It can suggest what to type next, and it never learns the grammar.** Slot 'helix-option' elements into 'suggestions' and the field shows them, moves through them with the arrows, and lets Enter press one; the option fires its own 'helix-select' and the consumer decides what choosing meant. Deciding *what* to suggest is theirs too: keys before a colon and values after it are the same panel with different contents, which is all a two-level filter is, and the parse that tells them apart belongs where the domain does. **The panel opens when there is something in it and closes when there is not**, rather than on a choice. A reader who picks a key wants its values next, and closing on the choice would shut the panel between two renders. Use 'selectionStart' and 'setSelectionRange' to write a completion back and put the caret after it, or the next parse reads the wrong term. */
|
|
8337
8366
|
"helix-headless-filter-input": DefineCustomElement<
|
|
8338
8367
|
HelixHeadlessFilterInput,
|
|
8339
8368
|
HelixHeadlessFilterInputVueProps,
|