@kubex/zinc 1.1.118 → 1.1.119
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/dist/custom-elements.json +1743 -1609
- package/dist/vscode.html-custom-data.json +138 -133
- package/dist/web-types.json +313 -303
- package/dist/zn.d.ts +54 -21
- package/dist/zn.min.js +334 -317
- package/docs/pages/components/form-group.md +2 -0
- package/package.json +1 -1
- package/src/components/form-group/form-group.component.ts +51 -86
- package/src/components/form-group/form-group.test.ts +21 -1
- package/src/components/remarkd-editor/actions.ts +2 -1
- package/src/components/remarkd-editor/remarkd-editor.component.ts +251 -9
- package/src/components/remarkd-editor/remarkd-editor.scss +49 -0
- package/src/components/remarkd-editor/remarkd-editor.test.ts +329 -5
|
@@ -362,6 +362,8 @@ This ensures forms remain usable on all devices without additional configuration
|
|
|
362
362
|
|
|
363
363
|
The label column sticks to the top of the scroll container, so on a long form it stays beside the inputs instead of scrolling away. Use the `--zn-form-group-sticky-top` custom property to change the offset it settles at — useful when the scroll container has a sticky header of its own.
|
|
364
364
|
|
|
365
|
+
A container that clips its overflow but never scrolls — a `zn-panel` body sized to its content, say — would otherwise catch the label and hold it still for the whole scroll. The group clips such a container instead (`overflow: clip`, which clips without being a scroll container), so the label follows the box that is actually scrolled. Scrolling goes straight back to the container the moment its content outgrows it.
|
|
366
|
+
|
|
365
367
|
```html:preview
|
|
366
368
|
<div style="max-height: 300px; overflow-y: auto;">
|
|
367
369
|
<zn-form-group label="Delivery Details" help-text="This label follows the inputs as you scroll">
|
package/package.json
CHANGED
|
@@ -45,19 +45,16 @@ export default class ZnFormGroup extends ZincElement {
|
|
|
45
45
|
|
|
46
46
|
@property({ attribute: 'pad', type: Boolean }) pad: boolean = false;
|
|
47
47
|
|
|
48
|
-
/** The
|
|
49
|
-
private
|
|
50
|
-
private stickyTop: number = 0;
|
|
48
|
+
/** The scroll containers we have clipped, against the inline overflow each carried before. */
|
|
49
|
+
private readonly clipped = new Map<HTMLElement, { x: string; y: string }>();
|
|
51
50
|
private frame: number = 0;
|
|
52
|
-
private rebind: boolean = false;
|
|
53
|
-
private offset: number = 0;
|
|
54
51
|
private resizeObserver: ResizeObserver | null = null;
|
|
55
52
|
|
|
56
53
|
connectedCallback() {
|
|
57
54
|
super.connectedCallback();
|
|
58
55
|
|
|
59
56
|
// Whether an ancestor scrolls depends on how tall this form has grown.
|
|
60
|
-
this.resizeObserver ??= new ResizeObserver(() => this.schedule(
|
|
57
|
+
this.resizeObserver ??= new ResizeObserver(() => this.schedule());
|
|
61
58
|
this.resizeObserver.observe(this);
|
|
62
59
|
window.addEventListener('resize', this.onViewportResize);
|
|
63
60
|
}
|
|
@@ -66,127 +63,95 @@ export default class ZnFormGroup extends ZincElement {
|
|
|
66
63
|
super.disconnectedCallback();
|
|
67
64
|
this.resizeObserver?.disconnect();
|
|
68
65
|
window.removeEventListener('resize', this.onViewportResize);
|
|
69
|
-
this.
|
|
66
|
+
this.release([...this.clipped.keys()]);
|
|
70
67
|
cancelAnimationFrame(this.frame);
|
|
71
68
|
this.frame = 0;
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
protected firstUpdated(changedProperties: PropertyValues) {
|
|
75
72
|
super.firstUpdated(changedProperties);
|
|
76
|
-
this.schedule(
|
|
73
|
+
this.schedule();
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
private get labelColumn(): HTMLElement | null {
|
|
80
77
|
return this.shadowRoot?.querySelector('.form-control__text') ?? null;
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
/** Coalesces
|
|
84
|
-
private schedule(
|
|
85
|
-
this.rebind ||= rebind;
|
|
80
|
+
/** Coalesces resize work into one frame, and out of the ResizeObserver callback. */
|
|
81
|
+
private schedule() {
|
|
86
82
|
if (this.frame) return;
|
|
87
83
|
|
|
88
84
|
this.frame = requestAnimationFrame(() => {
|
|
89
85
|
this.frame = 0;
|
|
90
|
-
|
|
91
|
-
this.rebind = false;
|
|
92
|
-
this.findScroller();
|
|
93
|
-
}
|
|
94
|
-
this.positionLabel();
|
|
86
|
+
this.freeSticky();
|
|
95
87
|
});
|
|
96
88
|
}
|
|
97
89
|
|
|
90
|
+
// A shorter viewport can make an ancestor scroll without changing this form's size.
|
|
91
|
+
private readonly onViewportResize = () => this.schedule();
|
|
92
|
+
|
|
98
93
|
/**
|
|
99
|
-
* Native sticky
|
|
100
|
-
*
|
|
101
|
-
*
|
|
94
|
+
* Native sticky anchors to the nearest scroll container, even one that cannot scroll — a `zn-panel` body sized to
|
|
95
|
+
* its content, say — where it then holds the label still for the whole scroll. `overflow: clip` clips without
|
|
96
|
+
* making a scroll container, so clipping those takes them out of sticky's search and the label follows the box the
|
|
97
|
+
* user actually scrolls, moved by the compositor rather than by hand.
|
|
102
98
|
*/
|
|
103
|
-
private
|
|
99
|
+
private freeSticky() {
|
|
104
100
|
const column = this.labelColumn;
|
|
105
101
|
if (!column) return;
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
this.stickyTop = parseFloat(getComputedStyle(column).top) || 0;
|
|
109
|
-
|
|
110
|
-
const anchor = this.nearestScrollContainer(column);
|
|
111
|
-
const scroller = anchor ? this.scrollingAncestor(column) : null;
|
|
112
|
-
const anchored = !anchor || scroller === anchor;
|
|
113
|
-
|
|
114
|
-
this.trackScroller(anchored ? null : scroller);
|
|
115
|
-
|
|
116
|
-
// A `top` inset against a box that never scrolls has nothing to hold the label back from:
|
|
117
|
-
// it only pushes the label down the page, so drop it and let the transform do the work.
|
|
118
|
-
if (!anchored) column.style.top = '0px';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private trackScroller(scroller: HTMLElement | null) {
|
|
122
|
-
if (scroller === this.tracked) return;
|
|
123
|
-
|
|
124
|
-
this.scrollTarget(this.tracked)?.removeEventListener('scroll', this.onScroll);
|
|
125
|
-
this.tracked = scroller;
|
|
126
|
-
this.scrollTarget(this.tracked)?.addEventListener('scroll', this.onScroll, { passive: true });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** The document scrolls through the window, every other scroller reports its own events. */
|
|
130
|
-
private scrollTarget(scroller: HTMLElement | null): EventTarget | null {
|
|
131
|
-
if (!scroller) return null;
|
|
132
|
-
return scroller === document.scrollingElement ? window : scroller;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private readonly onScroll = () => this.schedule();
|
|
103
|
+
const dead: HTMLElement[] = [];
|
|
136
104
|
|
|
137
|
-
|
|
138
|
-
|
|
105
|
+
for (const element of this.ancestors(column)) {
|
|
106
|
+
if (!this.clipped.has(element) && !this.isStickyAnchor(element)) continue;
|
|
139
107
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!column || !fieldset) return;
|
|
144
|
-
|
|
145
|
-
let offset = 0;
|
|
146
|
-
|
|
147
|
-
// The stylesheet drops sticky while the columns are stacked; tracking has to stand down too.
|
|
148
|
-
if (this.tracked && getComputedStyle(column).position === 'sticky') {
|
|
149
|
-
const visibleTop = this.tracked === document.scrollingElement
|
|
150
|
-
? 0
|
|
151
|
-
: this.tracked.getBoundingClientRect().top;
|
|
152
|
-
const restingTop = column.getBoundingClientRect().top - this.offset;
|
|
153
|
-
const travel = Math.max(0, fieldset.clientHeight - column.offsetHeight);
|
|
108
|
+
// Scroll size reports the overflow through a clip, so a box that has grown into needing to scroll is handed
|
|
109
|
+
// straight back — and sticky anchors to it, which is now the right answer.
|
|
110
|
+
if (this.overflows(element)) break;
|
|
154
111
|
|
|
155
|
-
|
|
112
|
+
dead.push(element);
|
|
156
113
|
}
|
|
157
114
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this.offset = offset;
|
|
161
|
-
column.style.transform = offset ? `translateY(${offset}px)` : '';
|
|
115
|
+
this.release([...this.clipped.keys()].filter(element => !dead.includes(element)));
|
|
116
|
+
dead.filter(element => !this.clipped.has(element)).forEach(element => this.clip(element));
|
|
162
117
|
}
|
|
163
118
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
119
|
+
private clip(element: HTMLElement) {
|
|
120
|
+
// A second form group in the same container finds the first one's clip already inline. Recording it as what was
|
|
121
|
+
// there before would leave the box clipped for good, so it counts as nothing to put back.
|
|
122
|
+
const kept = (overflow: string) => overflow === 'clip' ? '' : overflow;
|
|
123
|
+
this.clipped.set(element, { x: kept(element.style.overflowX), y: kept(element.style.overflowY) });
|
|
124
|
+
element.style.overflowX = 'clip';
|
|
125
|
+
element.style.overflowY = 'clip';
|
|
126
|
+
|
|
127
|
+
// Scrolling has to go back the moment the box is short enough to need it.
|
|
128
|
+
this.resizeObserver?.observe(element);
|
|
170
129
|
}
|
|
171
130
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
131
|
+
private release(elements: HTMLElement[]) {
|
|
132
|
+
elements.forEach(element => {
|
|
133
|
+
const inline = this.clipped.get(element);
|
|
134
|
+
element.style.overflowX = inline?.x ?? '';
|
|
135
|
+
element.style.overflowY = inline?.y ?? '';
|
|
136
|
+
this.resizeObserver?.unobserve(element);
|
|
137
|
+
this.clipped.delete(element);
|
|
178
138
|
});
|
|
139
|
+
}
|
|
179
140
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
return
|
|
141
|
+
/** The box native sticky would anchor to, whether or not it can be scrolled. */
|
|
142
|
+
private isStickyAnchor(element: HTMLElement) {
|
|
143
|
+
const style = getComputedStyle(element);
|
|
144
|
+
return this.isScrollContainer(style.overflowY) || this.isScrollContainer(style.overflowX);
|
|
184
145
|
}
|
|
185
146
|
|
|
186
147
|
private isScrollContainer(overflow: string) {
|
|
187
148
|
return overflow === 'auto' || overflow === 'scroll' || overflow === 'hidden' || overflow === 'overlay';
|
|
188
149
|
}
|
|
189
150
|
|
|
151
|
+
private overflows(element: HTMLElement) {
|
|
152
|
+
return element.scrollHeight > element.clientHeight + 1 || element.scrollWidth > element.clientWidth + 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
190
155
|
/** Walks the flattened tree, so slots and shadow boundaries are crossed the way layout does. */
|
|
191
156
|
private ancestors(from: HTMLElement): HTMLElement[] {
|
|
192
157
|
const out: HTMLElement[] = [];
|
|
@@ -78,8 +78,9 @@ describe('<zn-form-group>', () => {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
it('leaves the label level with the inputs while nothing scrolls', async () => {
|
|
81
|
+
// Held clear of the viewport's top edge, which sticky's own inset holds the label back from wherever it is.
|
|
81
82
|
const el = await fixture<HTMLElement>(html`
|
|
82
|
-
<div style="width: 900px">
|
|
83
|
+
<div style="width: 900px; padding-top: 60px">
|
|
83
84
|
<zn-panel>
|
|
84
85
|
<zn-form-group label="Sticky"><zn-input label="Name"></zn-input></zn-form-group>
|
|
85
86
|
</zn-panel>
|
|
@@ -93,6 +94,25 @@ describe('<zn-form-group>', () => {
|
|
|
93
94
|
expect(label).to.be.closeTo(inputs, 2);
|
|
94
95
|
});
|
|
95
96
|
|
|
97
|
+
it('clips a scroll container that cannot scroll, and hands it back when it can', async () => {
|
|
98
|
+
const el = await fixture<HTMLElement>(html`
|
|
99
|
+
<div style="width: 900px; max-height: 300px; overflow-y: auto">
|
|
100
|
+
<zn-panel>
|
|
101
|
+
<zn-form-group label="Sticky"></zn-form-group>
|
|
102
|
+
</zn-panel>
|
|
103
|
+
</div>`);
|
|
104
|
+
el.querySelector('zn-form-group')!.innerHTML = tallForm;
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
106
|
+
|
|
107
|
+
const body = el.querySelector('zn-panel')!.shadowRoot!.querySelector<HTMLElement>('.panel__body')!;
|
|
108
|
+
expect(getComputedStyle(body).overflowY, 'sticky has to skip a body that cannot scroll').to.equal('clip');
|
|
109
|
+
|
|
110
|
+
body.style.maxHeight = '120px';
|
|
111
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
112
|
+
|
|
113
|
+
expect(getComputedStyle(body).overflowY, 'and it scrolls again once it must').to.equal('auto');
|
|
114
|
+
});
|
|
115
|
+
|
|
96
116
|
it('holds the label in view when a panel sits between the form and the scroll container', async () => {
|
|
97
117
|
const el = await fixture<HTMLElement>(html`
|
|
98
118
|
<div style="max-height: 300px; overflow-y: auto">
|
|
@@ -25,7 +25,7 @@ export interface EditorAction {
|
|
|
25
25
|
/** Where the caret lands within `prefix`. Defaults to the end. */
|
|
26
26
|
caretOffset?: number;
|
|
27
27
|
/** Actions that open their own picker instead of inserting text. */
|
|
28
|
-
opens?: 'image' | 'include';
|
|
28
|
+
opens?: 'image' | 'include' | 'link';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Toolbar order, most-used first — the last groups are the first to collapse. */
|
|
@@ -128,6 +128,7 @@ export const EDITOR_ACTIONS: EditorAction[] = [
|
|
|
128
128
|
{key: 'tooltip', label: 'Tooltip', icon: 'message-circle-question-mark@lu', group: 'inline', keywords: ['term'], inline: {before: '{', after: '}(Explanation)', placeholder: 'Term'}},
|
|
129
129
|
{key: 'cross-reference', label: 'Cross reference', icon: 'link-2@lu', group: 'inline', keywords: ['xref'], inline: {before: '<<', after: '>>', placeholder: 'section,Label'}},
|
|
130
130
|
{key: 'links-and-images', label: 'Link', icon: 'link@lu', group: 'inline', inline: {before: '[', after: '](https://)', placeholder: 'Label'}},
|
|
131
|
+
{key: 'document-link', label: 'Link to article', icon: 'file-symlink@lu', group: 'inline', keywords: ['article', 'document', 'kb'], opens: 'link'},
|
|
131
132
|
{key: 'passthrough', label: 'Passthrough', icon: 'shield@lu', group: 'inline', keywords: ['raw', 'literal'], inline: {before: 'pass:[', after: ']', placeholder: 'raw'}},
|
|
132
133
|
{key: 'curly-bang-passthrough', label: 'Literal braces', icon: 'braces@lu', group: 'inline', inline: {before: '{!', after: '!}', placeholder: 'raw'}},
|
|
133
134
|
];
|