mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
110 lines
3.8 KiB
JavaScript
110 lines
3.8 KiB
JavaScript
const STEPS = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
|
|
|
|
/** Pure value navigation, also usable by a future template-based view. */
|
|
export function nextSegmentValue(values, value, key, { homeEnd = true } = {}) {
|
|
if (!values.length) return undefined;
|
|
if (homeEnd && key === 'Home') return values[0];
|
|
if (homeEnd && key === 'End') return values.at(-1);
|
|
const step = STEPS[key];
|
|
if (!step) return undefined;
|
|
return values[(values.indexOf(value) + step + values.length) % values.length];
|
|
}
|
|
|
|
/** Paint controlled selection without emitting changes or moving focus. */
|
|
export function syncSegmentedControl(
|
|
buttons,
|
|
value,
|
|
{ valueOf, activeClass = 'active', rovingFocus = false } = {},
|
|
) {
|
|
for (const button of buttons || []) {
|
|
if (!button) continue;
|
|
const selected = valueOf(button) === value;
|
|
if (button.classList.contains(activeClass) !== selected)
|
|
button.classList.toggle(activeClass, selected);
|
|
const set = (name, content) => {
|
|
if (button.getAttribute?.(name) !== content)
|
|
button.setAttribute(name, content);
|
|
};
|
|
set('aria-checked', String(selected));
|
|
if (rovingFocus) set('tabindex', selected ? '0' : '-1');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Controlled segmented-selector DOM adapter. Feature owners supply values and
|
|
* receive changes; rendering a restored value never triggers the callback.
|
|
* `keyboard` retains each existing group's policy: none, arrows, or radio
|
|
* (arrows plus Home/End). Pointer and native Enter/Space share the click path.
|
|
* An injected listener owner keeps the feature's existing teardown boundary.
|
|
* @param {object} options
|
|
* @param {Iterable<HTMLButtonElement>} options.buttons
|
|
* @param {Function} options.valueOf Maps a button to the feature's value.
|
|
* @param {Function} options.onChange Receives value and originating button.
|
|
* @param {'none'|'arrows'|'radio'} [options.keyboard]
|
|
* @param {HTMLElement} [options.keyTarget] Delegated keyboard target, if any.
|
|
* @param {Function} [options.readValue] Current controlled value for delegation.
|
|
* @param {Function} [options.listen] Feature-owned event registration.
|
|
* @returns {{ update: Function, destroy: Function }}
|
|
*/
|
|
export function createSegmentedControl({
|
|
buttons,
|
|
valueOf,
|
|
onChange,
|
|
keyboard = 'none',
|
|
keyTarget,
|
|
readValue,
|
|
listen,
|
|
}) {
|
|
const options = [...buttons].filter(Boolean);
|
|
const removers = [];
|
|
let destroyed = false;
|
|
const bind = (target, type, handler) => {
|
|
const owned = (event) => {
|
|
if (!destroyed) handler(event);
|
|
};
|
|
if (listen) listen(target, type, owned);
|
|
else {
|
|
target.addEventListener(type, owned);
|
|
removers.push(() => target.removeEventListener(type, owned));
|
|
}
|
|
};
|
|
for (const button of options)
|
|
bind(button, 'click', () => {
|
|
if (!button.disabled) onChange(valueOf(button), button);
|
|
});
|
|
const navigate = (event, from) => {
|
|
const enabled = options.filter((button) => !button.disabled);
|
|
const values = enabled.map(valueOf);
|
|
const next = nextSegmentValue(values, from, event.key, {
|
|
homeEnd: keyboard === 'radio',
|
|
});
|
|
if (next === undefined) return;
|
|
const button = enabled.find((option) => valueOf(option) === next);
|
|
event.preventDefault();
|
|
if (keyTarget) {
|
|
button.click();
|
|
button.focus();
|
|
} else {
|
|
button.focus();
|
|
if (next !== from) button.click();
|
|
}
|
|
};
|
|
if (keyboard !== 'none') {
|
|
if (keyTarget)
|
|
bind(keyTarget, 'keydown', (event) => navigate(event, readValue()));
|
|
else
|
|
for (const button of options)
|
|
bind(button, 'keydown', (event) => navigate(event, valueOf(button)));
|
|
}
|
|
return {
|
|
update(value, presentation) {
|
|
if (!destroyed)
|
|
syncSegmentedControl(options, value, { valueOf, ...presentation });
|
|
},
|
|
destroy() {
|
|
destroyed = true;
|
|
for (const remove of removers.splice(0)) remove();
|
|
},
|
|
};
|
|
}
|