|
import { addMeltEventListener, makeElement, createElHelpers, disabledAttr, executeCallbacks, getElemDirection, handleRovingFocus, isHTMLElement, kbd, noop, omit, overridable, toWritableStores, } from '../../internal/helpers/index.js'; |
|
import { derived, writable } from 'svelte/store'; |
|
const defaults = { |
|
type: 'single', |
|
orientation: 'horizontal', |
|
loop: true, |
|
rovingFocus: true, |
|
disabled: false, |
|
defaultValue: '', |
|
}; |
|
const { name, selector } = createElHelpers('toggle-group'); |
|
export const createToggleGroup = (props) => { |
|
const withDefaults = { ...defaults, ...props }; |
|
const options = toWritableStores(omit(withDefaults, 'value')); |
|
const { type, orientation, loop, rovingFocus, disabled } = options; |
|
const defaultValue = withDefaults.defaultValue |
|
? withDefaults.defaultValue |
|
: withDefaults.type === 'single' |
|
? undefined |
|
: []; |
|
const valueWritable = withDefaults.value ?? writable(defaultValue); |
|
const value = overridable(valueWritable, withDefaults?.onValueChange); |
|
const root = makeElement(name(), { |
|
stores: orientation, |
|
returned: ($orientation) => { |
|
return { |
|
role: 'group', |
|
'data-orientation': $orientation, |
|
}; |
|
}, |
|
}); |
|
const item = makeElement(name('item'), { |
|
stores: [value, disabled, orientation, type], |
|
returned: ([$value, $disabled, $orientation, $type]) => { |
|
return (props) => { |
|
const itemValue = typeof props === 'string' ? props : props.value; |
|
const argDisabled = typeof props === 'string' ? false : !!props.disabled; |
|
const disabled = $disabled || argDisabled; |
|
const pressed = Array.isArray($value) ? $value.includes(itemValue) : $value === itemValue; |
|
const isSingle = $type === 'single'; |
|
const isMultiple = $type === 'multiple' || $type === undefined; |
|
return { |
|
disabled: disabledAttr(disabled), |
|
pressed, |
|
'data-orientation': $orientation, |
|
'data-disabled': disabledAttr(disabled), |
|
'data-state': pressed ? 'on' : 'off', |
|
'data-value': itemValue, |
|
'aria-pressed': isMultiple ? pressed : undefined, |
|
'aria-checked': isSingle ? pressed : undefined, |
|
type: 'button', |
|
role: isSingle ? 'radio' : undefined, |
|
tabindex: pressed ? 0 : -1, |
|
}; |
|
}; |
|
}, |
|
action: (node) => { |
|
let unsub = noop; |
|
const parentGroup = node.closest(selector()); |
|
if (!isHTMLElement(parentGroup)) |
|
return {}; |
|
const items = Array.from(parentGroup.querySelectorAll(selector('item'))); |
|
const $value = value.get(); |
|
const anyPressed = Array.isArray($value) ? $value.length > 0 : $value ? true : false; |
|
if (!anyPressed && items[0] === node) { |
|
node.tabIndex = 0; |
|
} |
|
function getNodeProps() { |
|
const itemValue = node.dataset.value; |
|
const disabled = node.dataset.disabled === 'true'; |
|
return { value: itemValue, disabled }; |
|
} |
|
function handleValueUpdate() { |
|
const { value: itemValue, disabled } = getNodeProps(); |
|
if (itemValue === undefined || disabled) |
|
return; |
|
value.update(($value) => { |
|
if (Array.isArray($value)) { |
|
if ($value.includes(itemValue)) { |
|
return $value.filter((i) => i !== itemValue); |
|
} |
|
return [...$value, itemValue]; |
|
} |
|
return $value === itemValue ? undefined : itemValue; |
|
}); |
|
} |
|
unsub = executeCallbacks(addMeltEventListener(node, 'click', () => { |
|
handleValueUpdate(); |
|
}), addMeltEventListener(node, 'keydown', (e) => { |
|
if (e.key === kbd.SPACE || e.key === kbd.ENTER) { |
|
e.preventDefault(); |
|
handleValueUpdate(); |
|
return; |
|
} |
|
if (!rovingFocus.get()) |
|
return; |
|
const el = e.currentTarget; |
|
if (!isHTMLElement(el)) |
|
return; |
|
const root = el.closest(selector()); |
|
if (!isHTMLElement(root)) |
|
return; |
|
const items = Array.from(root.querySelectorAll(selector('item') + ':not([data-disabled])')).filter((item) => isHTMLElement(item)); |
|
const currentIndex = items.indexOf(el); |
|
const dir = getElemDirection(el); |
|
const $orientation = orientation.get(); |
|
const nextKey = { |
|
horizontal: dir === 'rtl' ? kbd.ARROW_LEFT : kbd.ARROW_RIGHT, |
|
vertical: kbd.ARROW_DOWN, |
|
}[$orientation ?? 'horizontal']; |
|
const prevKey = { |
|
horizontal: dir === 'rtl' ? kbd.ARROW_RIGHT : kbd.ARROW_LEFT, |
|
vertical: kbd.ARROW_UP, |
|
}[$orientation ?? 'horizontal']; |
|
const $loop = loop.get(); |
|
if (e.key === nextKey) { |
|
e.preventDefault(); |
|
const nextIndex = currentIndex + 1; |
|
if (nextIndex >= items.length && $loop) { |
|
handleRovingFocus(items[0]); |
|
} |
|
else { |
|
handleRovingFocus(items[nextIndex]); |
|
} |
|
} |
|
else if (e.key === prevKey) { |
|
e.preventDefault(); |
|
const prevIndex = currentIndex - 1; |
|
if (prevIndex < 0 && $loop) { |
|
handleRovingFocus(items[items.length - 1]); |
|
} |
|
else { |
|
handleRovingFocus(items[prevIndex]); |
|
} |
|
} |
|
else if (e.key === kbd.HOME) { |
|
e.preventDefault(); |
|
handleRovingFocus(items[0]); |
|
} |
|
else if (e.key === kbd.END) { |
|
e.preventDefault(); |
|
handleRovingFocus(items[items.length - 1]); |
|
} |
|
})); |
|
return { |
|
destroy: unsub, |
|
}; |
|
}, |
|
}); |
|
const isPressed = derived(value, ($value) => { |
|
return (itemValue) => { |
|
return Array.isArray($value) ? $value.includes(itemValue) : $value === itemValue; |
|
}; |
|
}); |
|
return { |
|
elements: { |
|
root, |
|
item, |
|
}, |
|
states: { |
|
value, |
|
}, |
|
helpers: { |
|
isPressed, |
|
}, |
|
options, |
|
}; |
|
}; |
|
|