tosijs-ui 1.7.2 → 1.7.4
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/CHANGELOG.md +46 -0
- package/dist/color-input.d.ts +0 -1
- package/dist/color-input.js +24 -10
- package/dist/doc-browser.d.ts +9 -0
- package/dist/doc-browser.js +54 -9
- package/dist/doc-system/doc-system-styles.d.ts +2 -0
- package/dist/doc-system/doc-system-styles.js +15 -1
- package/dist/doc-system/doc-system.js +1 -0
- package/dist/doc-system/site/check-examples-cli.js +5 -3
- package/dist/doc-system/site/check-examples.d.ts +13 -0
- package/dist/doc-system/site/check-examples.js +26 -10
- package/dist/doc-system/site/generate-site.d.ts +2 -0
- package/dist/doc-system/site/generate-site.js +1 -0
- package/dist/doc-system/site/orchestrator.js +16 -2
- package/dist/doc-system/site/site-config.d.ts +7 -0
- package/dist/form.d.ts +0 -1
- package/dist/form.js +35 -21
- package/dist/hydrate/hydrate.js +82 -82
- package/dist/hydrate/hydrate.js.map +16 -15
- package/dist/iife.js +114 -114
- package/dist/iife.js.map +16 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-example/code-transform.d.ts +14 -0
- package/dist/live-example/code-transform.js +20 -2
- package/dist/live-example/component.d.ts +24 -51
- package/dist/live-example/component.js +44 -51
- package/dist/live-example/styles.d.ts +22 -50
- package/dist/live-example/styles.js +48 -49
- package/dist/live-example/types.d.ts +2 -2
- package/dist/pocket-bar.d.ts +99 -0
- package/dist/pocket-bar.js +298 -0
- package/dist/pop-float.d.ts +1 -1
- package/dist/pop-float.js +13 -7
- package/dist/segmented.d.ts +0 -1
- package/dist/segmented.js +38 -20
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +3 -1
- package/package.json +3 -3
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Component as WebComponent, ElementCreator, PartsMap } from 'tosijs';
|
|
2
|
+
import { SvgIcon } from './icons';
|
|
3
|
+
interface PocketBarParts extends PartsMap {
|
|
4
|
+
handle: HTMLButtonElement;
|
|
5
|
+
handleIcon: SvgIcon;
|
|
6
|
+
bar: HTMLDivElement;
|
|
7
|
+
}
|
|
8
|
+
export declare class TosiPocketBar extends WebComponent<PocketBarParts> {
|
|
9
|
+
static preferredTagName: string;
|
|
10
|
+
static initAttributes: {
|
|
11
|
+
icon: string;
|
|
12
|
+
direction: string;
|
|
13
|
+
open: boolean;
|
|
14
|
+
};
|
|
15
|
+
private pinned;
|
|
16
|
+
private get vertical();
|
|
17
|
+
private get resolvedIcon();
|
|
18
|
+
private get floatPosition();
|
|
19
|
+
reposition: () => void;
|
|
20
|
+
private setOpen;
|
|
21
|
+
private handlePointerEnter;
|
|
22
|
+
private handlePointerLeave;
|
|
23
|
+
private handleFocusIn;
|
|
24
|
+
private handleFocusOut;
|
|
25
|
+
toggle: (event?: Event) => void;
|
|
26
|
+
private handleOutsidePointer;
|
|
27
|
+
private handleScrollResize;
|
|
28
|
+
content: () => (HTMLDivElement | HTMLButtonElement)[];
|
|
29
|
+
static shadowStyleSpec: {
|
|
30
|
+
':host': {
|
|
31
|
+
display: string;
|
|
32
|
+
position: string;
|
|
33
|
+
opacity: string;
|
|
34
|
+
transition: string;
|
|
35
|
+
};
|
|
36
|
+
':host([open])': {
|
|
37
|
+
opacity: string;
|
|
38
|
+
};
|
|
39
|
+
':host [part="handle"]': {
|
|
40
|
+
display: string;
|
|
41
|
+
alignItems: string;
|
|
42
|
+
justifyContent: string;
|
|
43
|
+
padding: string;
|
|
44
|
+
border: string;
|
|
45
|
+
margin: string;
|
|
46
|
+
cursor: string;
|
|
47
|
+
color: string;
|
|
48
|
+
background: string;
|
|
49
|
+
backdropFilter: string;
|
|
50
|
+
borderRadius: string;
|
|
51
|
+
transition: string;
|
|
52
|
+
};
|
|
53
|
+
':host [part="bar"]': {
|
|
54
|
+
position: string;
|
|
55
|
+
display: string;
|
|
56
|
+
gap: string;
|
|
57
|
+
padding: string;
|
|
58
|
+
background: string;
|
|
59
|
+
backdropFilter: string;
|
|
60
|
+
borderRadius: string;
|
|
61
|
+
boxShadow: string;
|
|
62
|
+
opacity: string;
|
|
63
|
+
pointerEvents: string;
|
|
64
|
+
transform: string;
|
|
65
|
+
transition: string;
|
|
66
|
+
};
|
|
67
|
+
':host([open]) [part="bar"]': {
|
|
68
|
+
opacity: string;
|
|
69
|
+
pointerEvents: string;
|
|
70
|
+
transform: string;
|
|
71
|
+
};
|
|
72
|
+
'::slotted(button), ::slotted(label)': {
|
|
73
|
+
display: string;
|
|
74
|
+
alignItems: string;
|
|
75
|
+
gap: string;
|
|
76
|
+
padding: string;
|
|
77
|
+
margin: string;
|
|
78
|
+
border: string;
|
|
79
|
+
background: string;
|
|
80
|
+
color: string;
|
|
81
|
+
font: string;
|
|
82
|
+
cursor: string;
|
|
83
|
+
borderRadius: string;
|
|
84
|
+
transition: string;
|
|
85
|
+
};
|
|
86
|
+
'::slotted(button:hover), ::slotted(label:hover)': {
|
|
87
|
+
background: string;
|
|
88
|
+
};
|
|
89
|
+
'::slotted(label:has(input[type="checkbox"]:not(:checked)))': {
|
|
90
|
+
opacity: string;
|
|
91
|
+
filter: string;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
connectedCallback(): void;
|
|
95
|
+
disconnectedCallback(): void;
|
|
96
|
+
render(): void;
|
|
97
|
+
}
|
|
98
|
+
export declare const tosiPocketBar: ElementCreator<TosiPocketBar>;
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/*#
|
|
2
|
+
# pocket bar
|
|
3
|
+
|
|
4
|
+
A **pocket toolbar**: a single icon at rest that expands into a bar of your controls
|
|
5
|
+
on hover or tap, then tucks away again. It's translucent until you touch it, so it
|
|
6
|
+
stays out of the way when pinned over content.
|
|
7
|
+
|
|
8
|
+
Whatever you put inside is the toolbar — they're your light-DOM children, so *any*
|
|
9
|
+
control works: buttons, a checkbox, a segmented control. The component only supplies
|
|
10
|
+
the handle, the reveal, and the positioning.
|
|
11
|
+
|
|
12
|
+
Hover (or focus, or tap) peeks it open; **click the handle to keep it open**, and
|
|
13
|
+
click the handle again — or anywhere outside — to dismiss it.
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<div class="pocket-demo">
|
|
17
|
+
<tosi-pocket-bar direction="e">
|
|
18
|
+
<button title="Cut"><tosi-icon icon="scissors"></tosi-icon></button>
|
|
19
|
+
<button title="Copy"><tosi-icon icon="copy"></tosi-icon></button>
|
|
20
|
+
<button title="Paste"><tosi-icon icon="clipboard"></tosi-icon></button>
|
|
21
|
+
</tosi-pocket-bar>
|
|
22
|
+
|
|
23
|
+
<tosi-pocket-bar direction="s">
|
|
24
|
+
<button title="Bold"><tosi-icon icon="bold"></tosi-icon></button>
|
|
25
|
+
<button title="Italic"><tosi-icon icon="italic"></tosi-icon></button>
|
|
26
|
+
<button title="Underline"><tosi-icon icon="underline"></tosi-icon></button>
|
|
27
|
+
</tosi-pocket-bar>
|
|
28
|
+
|
|
29
|
+
<tosi-pocket-bar direction="w">
|
|
30
|
+
<button title="Zoom in"><tosi-icon icon="zoomIn"></tosi-icon></button>
|
|
31
|
+
<button title="Zoom out"><tosi-icon icon="zoomOut"></tosi-icon></button>
|
|
32
|
+
</tosi-pocket-bar>
|
|
33
|
+
|
|
34
|
+
<tosi-pocket-bar direction="n" icon="settings">
|
|
35
|
+
<label title="Snap to grid"><input type="checkbox" checked /><tosi-icon icon="grid"></tosi-icon></label>
|
|
36
|
+
<button title="Undo"><tosi-icon icon="cornerUpLeft"></tosi-icon></button>
|
|
37
|
+
<button title="Redo"><tosi-icon icon="cornerUpRight"></tosi-icon></button>
|
|
38
|
+
</tosi-pocket-bar>
|
|
39
|
+
</div>
|
|
40
|
+
```
|
|
41
|
+
```css
|
|
42
|
+
.preview .pocket-demo {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: space-around;
|
|
46
|
+
height: 260px;
|
|
47
|
+
padding: 60px 20px;
|
|
48
|
+
}
|
|
49
|
+
.preview .pocket-demo tosi-pocket-bar button,
|
|
50
|
+
.preview .pocket-demo tosi-pocket-bar label {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 4px;
|
|
54
|
+
padding: 5px;
|
|
55
|
+
border: 0;
|
|
56
|
+
border-radius: 6px;
|
|
57
|
+
background: transparent;
|
|
58
|
+
color: inherit;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
.preview .pocket-demo tosi-pocket-bar button:hover,
|
|
62
|
+
.preview .pocket-demo tosi-pocket-bar label:hover {
|
|
63
|
+
background: #8882;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Each handle above hints its axis — `⋯` for the horizontal bars (`e`, `w`), `⋮` for the
|
|
68
|
+
vertical ones (`n`, `s`) — and the fourth uses an explicit `settings` icon. Hover any of
|
|
69
|
+
them to see the bar grow in its `direction`.
|
|
70
|
+
|
|
71
|
+
## Attributes
|
|
72
|
+
|
|
73
|
+
- `icon` — the handle icon (any icon name or [composition](icon-composition)). By
|
|
74
|
+
default it hints the growth axis: `moreHorizontal` (⋯) for a horizontal bar,
|
|
75
|
+
`moreVertical` (⋮) for a vertical one.
|
|
76
|
+
- `direction` — where the bar grows, using the `FloatPosition` vocabulary
|
|
77
|
+
(`n | e | s | w | ne | nw | se | sw | en | wn | es | ws | side | auto`). `auto`
|
|
78
|
+
(the default) grows a **horizontal** bar toward the nearer edge of the screen.
|
|
79
|
+
`e` / `w` (and `en` / `es` / `wn` / `ws`) grow a horizontal bar; `n` / `s` (and
|
|
80
|
+
`ne` / `nw` / `se` / `sw`) grow a **vertical** one. The bar is placed
|
|
81
|
+
edge-contiguous with the handle via [positionFloat](popFloat).
|
|
82
|
+
- `open` — reflects the open state. Hover / focus / tap peek it open; clicking the
|
|
83
|
+
handle keeps it open until you click the handle again or click outside.
|
|
84
|
+
|
|
85
|
+
The handle is slightly translucent at rest (`--tosi-pocket-opacity`, default `0.75`)
|
|
86
|
+
and opaque while open; it keeps a blurred glass chip (`--tosi-pocket-bg`, a translucent
|
|
87
|
+
tint of `--tosi-bg`, so it follows the theme) so it stays legible over busy content.
|
|
88
|
+
|
|
89
|
+
### Styling hooks
|
|
90
|
+
|
|
91
|
+
- `--tosi-pocket-opacity` — resting opacity of the whole widget (default `0.75`).
|
|
92
|
+
- `--tosi-pocket-bg` — the glass background (a translucent tint of `--tosi-bg`).
|
|
93
|
+
- `--tosi-pocket-handle-color` — colour of just the collapsed handle icon, independent
|
|
94
|
+
of the bar's controls (e.g. to flag status on the handle alone). Default `inherit`.
|
|
95
|
+
|
|
96
|
+
Slotted `<button>`s and `<label>`s get flat icon-button styling by default. A
|
|
97
|
+
checkbox-as-icon — a `<label>` wrapping an `<input type=checkbox>` and an icon — greys
|
|
98
|
+
and desaturates until checked, so a toggle needs no extra CSS.
|
|
99
|
+
*/
|
|
100
|
+
/*{ "parent": "Components" }*/
|
|
101
|
+
import { Component as WebComponent, elements, vars, varDefault, } from 'tosijs';
|
|
102
|
+
import { svgIcon } from './icons';
|
|
103
|
+
import { positionFloat } from './pop-float';
|
|
104
|
+
const { button, div, slot } = elements;
|
|
105
|
+
export class TosiPocketBar extends WebComponent {
|
|
106
|
+
static preferredTagName = 'tosi-pocket-bar';
|
|
107
|
+
static initAttributes = {
|
|
108
|
+
icon: '',
|
|
109
|
+
direction: 'auto',
|
|
110
|
+
open: false,
|
|
111
|
+
};
|
|
112
|
+
// Whether the bar is *pinned* open by a click (vs. a transient hover/focus peek).
|
|
113
|
+
pinned = false;
|
|
114
|
+
// n/s grow a vertical bar; everything else (auto, e/w, side) is horizontal.
|
|
115
|
+
get vertical() {
|
|
116
|
+
return /^[ns]/.test(this.direction);
|
|
117
|
+
}
|
|
118
|
+
// Default handle hints the growth axis: ⋮ for a vertical bar, ⋯ for a horizontal
|
|
119
|
+
// one. An explicit `icon` always wins.
|
|
120
|
+
get resolvedIcon() {
|
|
121
|
+
return this.icon || (this.vertical ? 'moreVertical' : 'moreHorizontal');
|
|
122
|
+
}
|
|
123
|
+
// `auto` → positionFloat's `side` mode: a horizontal bar toward the nearer screen
|
|
124
|
+
// edge. Everything else is a FloatPosition passed straight through.
|
|
125
|
+
get floatPosition() {
|
|
126
|
+
return (this.direction === 'auto' ? 'side' : this.direction);
|
|
127
|
+
}
|
|
128
|
+
reposition = () => {
|
|
129
|
+
if (!this.hydrated)
|
|
130
|
+
return;
|
|
131
|
+
positionFloat(this.parts.bar, this.parts.handle, this.floatPosition, 'remain', 'remain');
|
|
132
|
+
};
|
|
133
|
+
// Open is driven authoritatively in JS (not CSS :hover), so an explicit close —
|
|
134
|
+
// clicking the handle again, or clicking outside — always wins over a lingering
|
|
135
|
+
// hover. Otherwise a click-to-open bar could never be dismissed while the pointer
|
|
136
|
+
// stayed over it.
|
|
137
|
+
setOpen(open) {
|
|
138
|
+
if (this.open !== open)
|
|
139
|
+
this.open = open;
|
|
140
|
+
if (open)
|
|
141
|
+
this.reposition();
|
|
142
|
+
}
|
|
143
|
+
handlePointerEnter = () => this.setOpen(true);
|
|
144
|
+
handlePointerLeave = () => {
|
|
145
|
+
if (!this.pinned)
|
|
146
|
+
this.setOpen(false);
|
|
147
|
+
};
|
|
148
|
+
handleFocusIn = () => this.setOpen(true);
|
|
149
|
+
handleFocusOut = () => {
|
|
150
|
+
// Close once focus has left the whole component (unless pinned) — next frame,
|
|
151
|
+
// after focus has landed on its new target.
|
|
152
|
+
requestAnimationFrame(() => {
|
|
153
|
+
if (!this.pinned && !this.matches(':focus-within'))
|
|
154
|
+
this.setOpen(false);
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
toggle = (event) => {
|
|
158
|
+
event?.preventDefault();
|
|
159
|
+
this.pinned = !this.pinned;
|
|
160
|
+
this.setOpen(this.pinned);
|
|
161
|
+
};
|
|
162
|
+
// A pointer-down anywhere outside a pinned bar dismisses it (composedPath so it
|
|
163
|
+
// works across the shadow boundary and the position:fixed bar).
|
|
164
|
+
handleOutsidePointer = (event) => {
|
|
165
|
+
if (!this.pinned)
|
|
166
|
+
return;
|
|
167
|
+
const path = event.composedPath();
|
|
168
|
+
if (!path.includes(this) && !path.includes(this.parts.bar)) {
|
|
169
|
+
this.pinned = false;
|
|
170
|
+
this.setOpen(false);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
handleScrollResize = () => {
|
|
174
|
+
if (this.open)
|
|
175
|
+
this.reposition();
|
|
176
|
+
};
|
|
177
|
+
content = () => [
|
|
178
|
+
button({
|
|
179
|
+
part: 'handle',
|
|
180
|
+
class: 'no-drag',
|
|
181
|
+
title: 'toolbar',
|
|
182
|
+
'aria-label': 'toolbar',
|
|
183
|
+
'aria-expanded': 'false',
|
|
184
|
+
onClick: this.toggle,
|
|
185
|
+
}, svgIcon({ part: 'handleIcon', icon: this.resolvedIcon })),
|
|
186
|
+
div({
|
|
187
|
+
part: 'bar',
|
|
188
|
+
role: 'toolbar',
|
|
189
|
+
// Set at construction (render() keeps it in sync if `direction` changes).
|
|
190
|
+
style: { flexDirection: this.vertical ? 'column' : 'row' },
|
|
191
|
+
}, slot()),
|
|
192
|
+
];
|
|
193
|
+
static shadowStyleSpec = {
|
|
194
|
+
':host': {
|
|
195
|
+
display: 'inline-flex',
|
|
196
|
+
position: 'relative',
|
|
197
|
+
// Slightly translucent at rest so it stays out of the way, fully opaque when
|
|
198
|
+
// open. The handle keeps a blurred glass chip even at rest (below), so it
|
|
199
|
+
// stays legible over busy content instead of a bare, hard-to-see icon.
|
|
200
|
+
opacity: varDefault.tosiPocketOpacity('0.75'),
|
|
201
|
+
transition: 'opacity 0.15s ease-out',
|
|
202
|
+
},
|
|
203
|
+
':host([open])': {
|
|
204
|
+
opacity: '1',
|
|
205
|
+
},
|
|
206
|
+
':host [part="handle"]': {
|
|
207
|
+
display: 'inline-flex',
|
|
208
|
+
alignItems: 'center',
|
|
209
|
+
justifyContent: 'center',
|
|
210
|
+
padding: vars.spacing50,
|
|
211
|
+
border: '0',
|
|
212
|
+
margin: '0',
|
|
213
|
+
cursor: 'pointer',
|
|
214
|
+
// The handle's colour is independently settable (the bar's controls stay
|
|
215
|
+
// neutral), so a consumer can e.g. flag status on just the collapsed icon.
|
|
216
|
+
color: varDefault.tosiPocketHandleColor('inherit'),
|
|
217
|
+
// A blurred "glass" chip tinted from --tosi-bg (theme-aware, light or dark).
|
|
218
|
+
// The backdrop blur keeps the icon readable over any background.
|
|
219
|
+
background: varDefault.tosiPocketBg('color-mix(in srgb, var(--tosi-bg, #fff) 85%, transparent)'),
|
|
220
|
+
backdropFilter: 'blur(12px)',
|
|
221
|
+
borderRadius: vars.spacing,
|
|
222
|
+
transition: 'background 0.15s ease-out',
|
|
223
|
+
},
|
|
224
|
+
':host [part="bar"]': {
|
|
225
|
+
position: 'fixed',
|
|
226
|
+
display: 'flex',
|
|
227
|
+
gap: vars.spacing25,
|
|
228
|
+
padding: vars.spacing25,
|
|
229
|
+
background: varDefault.tosiPocketBg('color-mix(in srgb, var(--tosi-bg, #fff) 82%, transparent)'),
|
|
230
|
+
backdropFilter: 'blur(12px)',
|
|
231
|
+
borderRadius: vars.spacing,
|
|
232
|
+
boxShadow: `0 ${vars.spacing25} ${vars.spacing} #0003`,
|
|
233
|
+
// Hidden and inert at rest; revealed when open.
|
|
234
|
+
opacity: '0',
|
|
235
|
+
pointerEvents: 'none',
|
|
236
|
+
transform: 'scale(0.9)',
|
|
237
|
+
transition: 'opacity 0.15s ease-out, transform 0.15s ease-out',
|
|
238
|
+
},
|
|
239
|
+
':host([open]) [part="bar"]': {
|
|
240
|
+
opacity: '1',
|
|
241
|
+
pointerEvents: 'auto',
|
|
242
|
+
transform: 'scale(1)',
|
|
243
|
+
},
|
|
244
|
+
// Sensible defaults for slotted controls so a bar of icon-buttons needs no
|
|
245
|
+
// extra CSS: flat icon-buttons with a subtle hover tint. A checkbox-as-icon
|
|
246
|
+
// (label wrapping an <input type=checkbox> + an icon) greys and desaturates
|
|
247
|
+
// until it's checked — the easy way to build a toggle. Consumers override any
|
|
248
|
+
// of this from the light DOM (higher specificity) or hide the native checkbox.
|
|
249
|
+
'::slotted(button), ::slotted(label)': {
|
|
250
|
+
display: 'inline-flex',
|
|
251
|
+
alignItems: 'center',
|
|
252
|
+
gap: vars.spacing25,
|
|
253
|
+
padding: vars.spacing50,
|
|
254
|
+
margin: '0',
|
|
255
|
+
border: '0',
|
|
256
|
+
background: 'transparent',
|
|
257
|
+
color: 'inherit',
|
|
258
|
+
font: 'inherit',
|
|
259
|
+
cursor: 'pointer',
|
|
260
|
+
borderRadius: vars.spacing25,
|
|
261
|
+
transition: 'background 0.15s ease-out, opacity 0.15s, filter 0.15s',
|
|
262
|
+
},
|
|
263
|
+
'::slotted(button:hover), ::slotted(label:hover)': {
|
|
264
|
+
background: 'color-mix(in srgb, currentColor 15%, transparent)',
|
|
265
|
+
},
|
|
266
|
+
'::slotted(label:has(input[type="checkbox"]:not(:checked)))': {
|
|
267
|
+
opacity: '0.45',
|
|
268
|
+
filter: 'grayscale(1)',
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
connectedCallback() {
|
|
272
|
+
super.connectedCallback();
|
|
273
|
+
this.addEventListener('pointerenter', this.handlePointerEnter);
|
|
274
|
+
this.addEventListener('pointerleave', this.handlePointerLeave);
|
|
275
|
+
this.addEventListener('focusin', this.handleFocusIn);
|
|
276
|
+
this.addEventListener('focusout', this.handleFocusOut);
|
|
277
|
+
document.addEventListener('pointerdown', this.handleOutsidePointer, true);
|
|
278
|
+
window.addEventListener('scroll', this.handleScrollResize, {
|
|
279
|
+
passive: true,
|
|
280
|
+
capture: true,
|
|
281
|
+
});
|
|
282
|
+
window.addEventListener('resize', this.handleScrollResize, { passive: true });
|
|
283
|
+
}
|
|
284
|
+
disconnectedCallback() {
|
|
285
|
+
super.disconnectedCallback();
|
|
286
|
+
document.removeEventListener('pointerdown', this.handleOutsidePointer, true);
|
|
287
|
+
window.removeEventListener('scroll', this.handleScrollResize, true);
|
|
288
|
+
window.removeEventListener('resize', this.handleScrollResize);
|
|
289
|
+
}
|
|
290
|
+
render() {
|
|
291
|
+
super.render();
|
|
292
|
+
this.parts.handleIcon.icon = this.resolvedIcon;
|
|
293
|
+
this.parts.bar.style.flexDirection = this.vertical ? 'column' : 'row';
|
|
294
|
+
this.parts.handle.setAttribute('aria-expanded', String(!!this.open));
|
|
295
|
+
this.reposition();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export const tosiPocketBar = TosiPocketBar.elementCreator();
|
package/dist/pop-float.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export interface PopFloatOptions {
|
|
|
11
11
|
draggable?: boolean;
|
|
12
12
|
}
|
|
13
13
|
export declare const popFloat: (options: PopFloatOptions) => TosiFloat;
|
|
14
|
-
export declare const positionFloat: (element:
|
|
14
|
+
export declare const positionFloat: (element: HTMLElement, target: HTMLElement, position?: FloatPosition, remainOnScroll?: "hide" | "remove" | "remain", remainOnResize?: "hide" | "remove" | "remain", draggable?: boolean) => void;
|
package/dist/pop-float.js
CHANGED
|
@@ -241,7 +241,7 @@ preview.append(button(
|
|
|
241
241
|
```
|
|
242
242
|
|
|
243
243
|
*/
|
|
244
|
-
import { tosiFloat } from './float';
|
|
244
|
+
import { tosiFloat, TosiFloat } from './float';
|
|
245
245
|
import { bringToFront } from './track-drag';
|
|
246
246
|
export const popFloat = (options) => {
|
|
247
247
|
const { content, target, position, remainOnScroll, remainOnResize, draggable, } = options;
|
|
@@ -255,19 +255,25 @@ export const popFloat = (options) => {
|
|
|
255
255
|
document.body.append(float);
|
|
256
256
|
return float;
|
|
257
257
|
};
|
|
258
|
-
export const positionFloat = (
|
|
258
|
+
export const positionFloat = (
|
|
259
|
+
// Any element can be positioned relative to a target — only the remain-on-* /
|
|
260
|
+
// drag behaviors below are TosiFloat-specific (guarded), the geometry is not.
|
|
261
|
+
// (A `<tosi-pocket-bar>` positions its plain bar element this way.)
|
|
262
|
+
element, target, position, remainOnScroll, remainOnResize, draggable = false) => {
|
|
259
263
|
{
|
|
260
264
|
const { position } = getComputedStyle(element);
|
|
261
265
|
if (position !== 'fixed') {
|
|
262
266
|
element.style.position = 'fixed';
|
|
263
267
|
}
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
if (element instanceof TosiFloat) {
|
|
269
|
+
if (remainOnResize)
|
|
270
|
+
element.remainOnResize = remainOnResize;
|
|
271
|
+
if (remainOnScroll)
|
|
272
|
+
element.remainOnScroll = remainOnScroll;
|
|
273
|
+
element.drag = draggable;
|
|
274
|
+
}
|
|
268
275
|
bringToFront(element);
|
|
269
276
|
}
|
|
270
|
-
element.drag = draggable;
|
|
271
277
|
const { left, top, width, height } = target.getBoundingClientRect();
|
|
272
278
|
const cx = left + width * 0.5;
|
|
273
279
|
const cy = top + height * 0.5;
|
package/dist/segmented.d.ts
CHANGED
package/dist/segmented.js
CHANGED
|
@@ -262,17 +262,20 @@ export class TosiSegmented extends WebComponent {
|
|
|
262
262
|
opacity: varDefault.segmentedPlaceholderOpacity(0.75),
|
|
263
263
|
},
|
|
264
264
|
};
|
|
265
|
-
// Reflects
|
|
266
|
-
//
|
|
267
|
-
//
|
|
265
|
+
// Reflects the selection onto each label as a `.current` class so the highlight
|
|
266
|
+
// updates reliably (see note on the `.current` style rule). Derived from
|
|
267
|
+
// `this.value` (the authoritative selection, matching render()'s `checked` logic),
|
|
268
|
+
// NOT from re-reading each `input.checked`: on Firefox/WebKit that live read can
|
|
269
|
+
// lag the native radio update at change time, leaving `.current` on the previous
|
|
270
|
+
// segment while `input.checked` (and `value`) are already correct.
|
|
268
271
|
syncCurrent() {
|
|
269
272
|
const { options } = this.parts;
|
|
273
|
+
const { values, isOtherValue } = this;
|
|
270
274
|
options.querySelectorAll('label').forEach((label) => {
|
|
271
|
-
const
|
|
272
|
-
label.classList.toggle('current',
|
|
275
|
+
const val = label.querySelector('input')?.value ?? '';
|
|
276
|
+
label.classList.toggle('current', values.includes(val) || (val === '' && isOtherValue));
|
|
273
277
|
});
|
|
274
278
|
}
|
|
275
|
-
valueChanged = false;
|
|
276
279
|
handleChange = () => {
|
|
277
280
|
const { options, custom } = this.parts;
|
|
278
281
|
if (this.multiple) {
|
|
@@ -298,7 +301,6 @@ export class TosiSegmented extends WebComponent {
|
|
|
298
301
|
}
|
|
299
302
|
}
|
|
300
303
|
this.syncCurrent();
|
|
301
|
-
this.valueChanged = true;
|
|
302
304
|
};
|
|
303
305
|
handleKey = (event) => {
|
|
304
306
|
let blockEvent = false;
|
|
@@ -378,29 +380,45 @@ export class TosiSegmented extends WebComponent {
|
|
|
378
380
|
}
|
|
379
381
|
render() {
|
|
380
382
|
super.render();
|
|
381
|
-
if (this.valueChanged) {
|
|
382
|
-
this.valueChanged = false;
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
383
|
const { options, custom } = this.parts;
|
|
386
|
-
options.textContent = '';
|
|
387
384
|
const type = this.multiple ? 'checkbox' : 'radio';
|
|
388
385
|
const { values, isOtherValue } = this;
|
|
389
|
-
|
|
390
|
-
|
|
386
|
+
const choices = this._choicesWithOther;
|
|
387
|
+
const isChecked = (value) => values.includes(value) || (value === '' && isOtherValue);
|
|
388
|
+
// Idempotent: rebuild the <label>s ONLY when the choice set changed; otherwise
|
|
389
|
+
// reconcile `checked` in place. Re-creating them every render (previously avoided
|
|
390
|
+
// by a `valueChanged` timing flag) dropped focus from the segment you just
|
|
391
|
+
// clicked and is provably redundant when the labels already match the choices —
|
|
392
|
+
// so render is now safe to run any number of times, any time.
|
|
393
|
+
const labels = [...options.querySelectorAll('label')];
|
|
394
|
+
const structureMatches = labels.length === choices.length &&
|
|
395
|
+
labels.every((l, i) => l.querySelector('input')?.value === choices[i].value);
|
|
396
|
+
if (structureMatches) {
|
|
397
|
+
labels.forEach((label) => {
|
|
398
|
+
const input = label.querySelector('input');
|
|
399
|
+
if (input && input.checked !== isChecked(input.value)) {
|
|
400
|
+
input.checked = isChecked(input.value);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
options.textContent = '';
|
|
406
|
+
options.append(...choices.map((choice) => label({ tabindex: 0 }, input({
|
|
391
407
|
type,
|
|
392
408
|
name: this.name,
|
|
393
409
|
value: choice.value,
|
|
394
|
-
checked:
|
|
395
|
-
(choice.value === '' && isOtherValue),
|
|
410
|
+
checked: isChecked(choice.value),
|
|
396
411
|
tabIndex: -1,
|
|
397
|
-
}), choice.icon || { class: 'no-icon' }, this.localized ? tosiLocalized(choice.caption) : span(choice.caption));
|
|
398
|
-
}
|
|
412
|
+
}), choice.icon || { class: 'no-icon' }, this.localized ? tosiLocalized(choice.caption) : span(choice.caption))));
|
|
413
|
+
}
|
|
399
414
|
if (this.other && !this.multiple) {
|
|
400
415
|
custom.hidden = !isOtherValue;
|
|
401
|
-
|
|
416
|
+
const customValue = isOtherValue ? this.value : '';
|
|
417
|
+
if (custom.value !== customValue)
|
|
418
|
+
custom.value = customValue;
|
|
402
419
|
custom.placeholder = this.placeholder;
|
|
403
|
-
|
|
420
|
+
if (custom.parentElement !== options)
|
|
421
|
+
options.append(custom);
|
|
404
422
|
}
|
|
405
423
|
this.syncCurrent();
|
|
406
424
|
}
|