@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.
@@ -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
- /** A group's accordion state: the shopper's toggle, else open with a selection, else the default (all, none, the first N of the sidebar). */
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
- return { id: groupId(route.id, name), open: view.open?.[name] ?? (selected || byDefault), toggle: () => view.toggleGroup?.(name) };
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
- if (track) {
191
- e.preventDefault();
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
- const move = (x: number) => {
197
- const { from: f, to: t } = values(p, r);
198
- const n = valueAt(x);
199
- show(p, r, thumb === p.lo ? Math.min(n, t) : f, thumb === p.lo ? t : Math.max(n, f));
200
- };
201
- move(e.clientX);
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
- const onMove = (m: PointerEvent) => m.pointerId === e.pointerId && move(m.clientX);
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 up = (u: PointerEvent) => {
225
+ const end = (u: PointerEvent) => {
211
226
  if (u.pointerId !== e.pointerId) return;
212
- window.removeEventListener('pointerup', up, true);
213
- window.removeEventListener('pointercancel', up, true);
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', up, true);
219
- window.addEventListener('pointercancel', up, true);
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%); }