@luigisbox/luma 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -9
- package/docs/ACCESSIBILITY.md +8 -3
- package/package.json +1 -1
- package/src/analytics/chain.ts +9 -1
- package/src/analytics/collect.ts +14 -7
- package/src/analytics/from-ga4.ts +5 -2
- package/src/analytics/from-luma.ts +6 -4
- package/src/analytics/index.ts +1 -1
- package/src/core/early-rules.ts +16 -3
- package/src/core/entries.ts +10 -6
- package/src/core/stash.ts +24 -4
- package/src/types.ts +11 -2
- package/src/widgets/autocomplete/index.ts +186 -31
- package/src/widgets/autocomplete/style-autocomplete.css +11 -5
- package/src/widgets/autocomplete/template-autocomplete-sheet.ts +1 -1
- package/src/widgets/listing/element.ts +209 -21
- package/src/widgets/listing/instant.ts +83 -23
- package/src/widgets/listing/stash.ts +31 -8
- package/src/widgets/listing/view.ts +12 -2
- package/src/widgets/search/range.ts +40 -18
- package/src/widgets/search/style-search.css +2 -2
|
@@ -46,6 +46,9 @@ export type ListingData = Omit<SearchScope, keyof Helpers | 'go' | 'toggleFacet'
|
|
|
46
46
|
export interface ListingView {
|
|
47
47
|
/** Groups opened or closed by the shopper, by facet name. */
|
|
48
48
|
open?: Record<string, boolean>;
|
|
49
|
+
/** Groups shown open for their selection earlier in the visit: they stay open with it gone (`keptOpen` records them). */
|
|
50
|
+
kept?: Record<string, boolean>;
|
|
51
|
+
keptOpen?: (name: string) => void;
|
|
49
52
|
/** Groups open by default (`facetGroups.open` at this width): all, none, or the first N of the sidebar. */
|
|
50
53
|
openDefault?: boolean | number;
|
|
51
54
|
/** A group's search text, by facet name. */
|
|
@@ -120,11 +123,18 @@ export const listingData = (ctx: Pick<WidgetContext<ListingConfig>, 'config' | '
|
|
|
120
123
|
const ranges: SearchScope['ranges'] = [];
|
|
121
124
|
const names = facetFields(cfg, q);
|
|
122
125
|
const layout = q.layout ? [...q.layout, ...names.filter((n) => !q.layout!.includes(n))] : names;
|
|
123
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* A group's accordion state: the shopper's toggle, else open with a
|
|
128
|
+
* selection (and from then on in the visit: unticking its last value does
|
|
129
|
+
* not collapse it under the finger), else the default (all, none, the
|
|
130
|
+
* first N of the sidebar).
|
|
131
|
+
*/
|
|
124
132
|
const group = (name: string, selected: boolean) => {
|
|
125
133
|
const at = layout.indexOf(name);
|
|
126
134
|
const byDefault = typeof openDefault === 'number' ? at >= 0 && at < openDefault : !!openDefault;
|
|
127
|
-
|
|
135
|
+
const toggled = view.open?.[name];
|
|
136
|
+
if (toggled == null && selected) view.keptOpen?.(name);
|
|
137
|
+
return { id: groupId(route.id, name), open: toggled ?? (selected || !!view.kept?.[name] || byDefault), toggle: () => view.toggleGroup?.(name) };
|
|
128
138
|
};
|
|
129
139
|
for (const name of names) {
|
|
130
140
|
const data = res.facets[name];
|
|
@@ -18,6 +18,8 @@ type Range = SearchScope['ranges'][number];
|
|
|
18
18
|
|
|
19
19
|
/** A pause after the last key before a thumb moved from the keyboard commits (one request for a run of presses). */
|
|
20
20
|
export const KEY_COMMIT_MS = 600;
|
|
21
|
+
/** How far a touch on the track goes sideways (and more than down) before it is a drag, not the start of a scroll. */
|
|
22
|
+
const TOUCH_SLOP_PX = 8;
|
|
21
23
|
|
|
22
24
|
interface Parts {
|
|
23
25
|
root: HTMLElement;
|
|
@@ -169,7 +171,10 @@ export const rangeHandlers = (r: Range) => {
|
|
|
169
171
|
/**
|
|
170
172
|
* A press on the slider: on a thumb, the native drag (committed when the
|
|
171
173
|
* pointer lets go); on the track, the nearer thumb jumps there and follows
|
|
172
|
-
* the pointer until it lets go.
|
|
174
|
+
* the pointer until it lets go. A touch (or pen) on the track may be the
|
|
175
|
+
* start of a scroll (touch-action: pan-y): nothing moves until the finger
|
|
176
|
+
* goes sideways (a drag) or lifts where it pressed (a tap). A cancelled
|
|
177
|
+
* press (the browser took the gesture) commits nothing.
|
|
173
178
|
*/
|
|
174
179
|
press: (e: PointerEvent) => {
|
|
175
180
|
const p = at(e);
|
|
@@ -187,36 +192,53 @@ export const rangeHandlers = (r: Range) => {
|
|
|
187
192
|
return snap(r.min + Math.min(1, Math.max(0, f)) * (r.max - r.min), r);
|
|
188
193
|
};
|
|
189
194
|
let thumb: HTMLInputElement | null = null;
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
const move = (x: number) => {
|
|
196
|
+
const { from: f, to: t } = values(p, r);
|
|
197
|
+
const n = valueAt(x);
|
|
198
|
+
show(p, r, thumb === p.lo ? Math.min(n, t) : f, thumb === p.lo ? t : Math.max(n, f));
|
|
199
|
+
};
|
|
200
|
+
/** The thumb nearer the press takes it, and goes to x. */
|
|
201
|
+
const grab = (x: number) => {
|
|
192
202
|
const v = valueAt(e.clientX);
|
|
193
203
|
const { from, to } = values(p, r);
|
|
194
|
-
thumb = Math.abs(v - from) < Math.abs(v - to) || (v < from && from === to) ? p.lo : p.hi
|
|
204
|
+
thumb = Math.abs(v - from) < Math.abs(v - to) || (v < from && from === to) ? p.lo! : p.hi!;
|
|
195
205
|
thumb.focus({ preventScroll: true });
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
move(x);
|
|
207
|
+
};
|
|
208
|
+
let onMove: ((m: PointerEvent) => void) | null = null;
|
|
209
|
+
if (track) {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
if (e.pointerType === 'mouse' || !e.pointerType) grab(e.clientX);
|
|
202
212
|
try {
|
|
203
213
|
p.slider.setPointerCapture(e.pointerId);
|
|
204
214
|
} catch {}
|
|
205
|
-
|
|
215
|
+
onMove = (m: PointerEvent) => {
|
|
216
|
+
if (m.pointerId !== e.pointerId) return;
|
|
217
|
+
if (!thumb) {
|
|
218
|
+
const dx = Math.abs(m.clientX - e.clientX);
|
|
219
|
+
if (dx < TOUCH_SLOP_PX || dx <= Math.abs(m.clientY - e.clientY)) return;
|
|
220
|
+
grab(m.clientX);
|
|
221
|
+
} else move(m.clientX);
|
|
222
|
+
};
|
|
206
223
|
p.slider.addEventListener('pointermove', onMove);
|
|
207
|
-
const done = () => p.slider!.removeEventListener('pointermove', onMove);
|
|
208
|
-
p.slider.addEventListener('lostpointercapture', done, { once: true });
|
|
209
224
|
}
|
|
210
|
-
const
|
|
225
|
+
const end = (u: PointerEvent) => {
|
|
211
226
|
if (u.pointerId !== e.pointerId) return;
|
|
212
|
-
window.removeEventListener('pointerup',
|
|
213
|
-
window.removeEventListener('pointercancel',
|
|
227
|
+
window.removeEventListener('pointerup', end, true);
|
|
228
|
+
window.removeEventListener('pointercancel', end, true);
|
|
229
|
+
if (onMove) p.slider!.removeEventListener('pointermove', onMove);
|
|
214
230
|
st.pressed = false;
|
|
231
|
+
if (u.type === 'pointercancel') {
|
|
232
|
+
// The browser took the gesture (a scroll): the track's thumb back where it was drawn. A thumb's own drag is the input's (its change commits, if any).
|
|
233
|
+
if (track && thumb) show(p, r, r.from, r.to);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (track && !thumb) grab(u.clientX); // a tap
|
|
215
237
|
// The native drag's own `change` may come after this: commit() sends a value once.
|
|
216
238
|
setTimeout(() => commit(p, r), 0);
|
|
217
239
|
};
|
|
218
|
-
window.addEventListener('pointerup',
|
|
219
|
-
window.addEventListener('pointercancel',
|
|
240
|
+
window.addEventListener('pointerup', end, true);
|
|
241
|
+
window.addEventListener('pointercancel', end, true);
|
|
220
242
|
},
|
|
221
243
|
/** Typed into an input: the thumbs follow (within the range); nothing is committed yet. */
|
|
222
244
|
numberInput: (e: Event) => {
|
|
@@ -42,8 +42,8 @@ lbx-search:focus, lbx-plp:focus { outline: none; }
|
|
|
42
42
|
.lbx-s__fsearch { margin: 4px 0 8px; }
|
|
43
43
|
.lbx-s__fsearch-input { box-sizing: border-box; width: 100%; min-height: 24px; font: inherit; }
|
|
44
44
|
.lbx-s__fsearch-n { display: block; min-height: 1.2em; font-size: .875em; }
|
|
45
|
-
/* A range facet's slider: its theming variables in template-search-range-default.ts. */
|
|
46
|
-
.lbx-s__range-slider { --lbx-rs: var(--lbx-range-thumb-size, 24px); --lbx-rt: var(--lbx-range-thumb, #fff); --lbx-rb: var(--lbx-range-thumb-border, currentColor); --lbx-rr: var(--lbx-range-ring, currentColor); position: relative; height: max(24px, var(--lbx-rs)); margin: 4px 0 8px; }
|
|
45
|
+
/* A range facet's slider: its theming variables in template-search-range-default.ts. touch-action: a horizontal touch drag is the slider's (a press on the track follows the finger), a vertical one still scrolls. */
|
|
46
|
+
.lbx-s__range-slider { --lbx-rs: var(--lbx-range-thumb-size, 24px); --lbx-rt: var(--lbx-range-thumb, #fff); --lbx-rb: var(--lbx-range-thumb-border, currentColor); --lbx-rr: var(--lbx-range-ring, currentColor); position: relative; height: max(24px, var(--lbx-rs)); margin: 4px 0 8px; touch-action: pan-y; }
|
|
47
47
|
.lbx-s__range-track { position: absolute; top: 50%; left: calc(var(--lbx-rs) / 2); right: calc(var(--lbx-rs) / 2); height: var(--lbx-range-height, 4px); transform: translateY(-50%); border-radius: 4px; background: var(--lbx-range-track, #c4c4c4); }
|
|
48
48
|
.lbx-s__range-fill { position: absolute; top: 0; bottom: 0; left: var(--lbx-from, 0%); right: calc(100% - var(--lbx-to, 100%)); border-radius: inherit; background: var(--lbx-range-fill, currentColor); }
|
|
49
49
|
[dir="rtl"] .lbx-s__range-fill { left: calc(100% - var(--lbx-to, 100%)); right: var(--lbx-from, 0%); }
|