UNPKG

vue-material-adapter

Version:

Vue 3 wrapper arround Material Components for the Web

324 lines (251 loc) 9.01 kB
import { MDCSelectFoundation } from '@material/select/foundation.js'; import { computed, onBeforeUnmount, onMounted, reactive, toRef, toRefs, watch, } from 'vue'; import { emitCustomEvent } from '../base/index.js'; import { useRipplePlugin } from '../ripple/ripple-plugin.js'; import SelectHelperText from './select-helper-text.js'; import SelectIcon from './select-icon.js'; const { strings } = MDCSelectFoundation; let uid_ = 0; export default { name: 'mcw-select', inheritAttrs: false, props: { modelValue: String, helptext: String, leadingIcon: String, helptextPersistent: Boolean, helptextValidation: Boolean, disabled: Boolean, label: String, outlined: Boolean, required: Boolean, menuFullwidth: { type: Boolean, default: () => true }, }, setup(props, { emit }) { const uiState = reactive({ styles: {}, classes: {}, selectedTextContent: '', selTextAttrs: {}, selectAnchorAttrs: {}, helpId: `help-mcw-select-${uid_++}`, menuClasses: { 'mdc-menu-surface--fullwidth': props.menuFullwidth }, root: undefined, helperTextEl: undefined, leadingIconEl: undefined, lineRippleEl: undefined, outlineEl: undefined, labelEl: undefined, menu: undefined, anchorEl: undefined, }); let rippleClasses; let rippleStyles; if (props.outlined) { const { classes, styles } = useRipplePlugin(toRef(uiState, 'anchorEl'), { registerInteractionHandler: (eventType, handler) => { uiState.anchorEl.addEventListener(eventType, handler); }, deregisterInteractionHandler: (eventType, handler) => { uiState.anchorEl.removeEventListener(eventType, handler); }, }); rippleClasses = classes; rippleStyles = styles; } const rootClasses = computed(() => { return { 'mdc-select': 1, 'mdc-select--required': props.required, 'mdc-select--filled': !props.outlined, 'mdc-select--outlined': props.outlined, 'mdc-select--with-leading-icon': props.leadingIcon, 'mdc-select--disabled': props.disabled, 'mdc-select--no-label': !props.label, ...uiState.classes, }; }); let foundation; let labelElement; const handleFocus = () => foundation.handleFocus(); const handleBlur = () => foundation.handleBlur(); const handleClick = event_ => { uiState.anchorEl.focus(); handleFocus(); foundation.handleClick(getNormalizedXCoordinate(event_)); }; const handleKeydown = event_ => foundation.handleKeydown(event_); const handleChange = isOpen => foundation[`handleMenu${isOpen ? 'Opened' : 'Closed'}`](); const layout = () => foundation.layout(); const handleMenuOpened = () => foundation.handleMenuOpened(); const handleMenuClosed = () => foundation.handleMenuClosed(); const handleMenuItemAction = ({ index }) => foundation.handleMenuItemAction(index); const layoutOptions = () => { foundation.layoutOptions(); uiState.menu.layout(); }; // const selectedTextAttributes = computed(() => { // const attributes = { ...uiState.selTextAttrs }; // if (props.helptext) { // attributes['aria-controls'] = uiState.helpId; // attributes['aria-describedBy'] = uiState.helpId; // } // return attributes; // }); if (props.helptext) { uiState.selectAnchorAttrs['aria-controls'] = uiState.helpId; uiState.selectAnchorAttrs['aria-describedBy'] = uiState.helpId; } const adapter = { getMenuItemAttr: (menuItem, attribute) => menuItem.getAttribute(attribute), setSelectedText: text => (uiState.selectedTextContent = text), isSelectAnchorFocused: () => document.activeElement === uiState.anchorEl, getSelectAnchorAttr: attribute => uiState.selectAnchorAttrs[attribute], setSelectAnchorAttr: (attribute, value) => (uiState.selectAnchorAttrs = { ...uiState.selectAnchorAttrs, [attribute]: value, }), removeSelectAnchorAttr: attribute => { const { [attribute]: removed, ...rest } = uiState.selectAnchorAttrs; uiState.selectAnchorAttrs = rest; }, addMenuClass: className => (uiState.menuClasses = { ...uiState.menuClasses, [className]: true }), removeMenuClass: className => { const { [className]: removed, ...rest } = uiState.menuClasses; uiState.menuClasses = rest; }, openMenu: () => (uiState.menu.surfaceOpen = true), closeMenu: () => (uiState.menu.surfaceOpen = false), getAnchorElement: () => uiState.anchorEl, setMenuAnchorElement: anchorElement => uiState.menu.setAnchorElement(anchorElement), setMenuAnchorCorner: anchorCorner => uiState.menu.setAnchorCorner(anchorCorner), setMenuWrapFocus: wrapFocus => (uiState.menu.wrapFocus = wrapFocus), getSelectedIndex: () => { const index = uiState.menu?.getSelectedIndex() ?? -1; return Array.isArray(index) ? index[0] : index; }, setSelectedIndex: index => uiState.menu.setSelectedIndex(index), focusMenuItemAtIndex: index => uiState.menu.focusItemAtIndex(index), getMenuItemCount: () => uiState.menu.getMenuItemCount(), getMenuItemValues: () => uiState.menu?.getMenuItemValues(strings.VALUE_ATTR), getMenuItemTextAtIndex: index => uiState.menu?.getMenuItemTextAtIndex(index), isTypeaheadInProgress: () => uiState.menu.typeaheadInProgress(), typeaheadMatchItem: (nextChar, startingIndex) => uiState.menu?.typeaheadMatchItem(nextChar, startingIndex), // common methods addClass: className => (uiState.classes = { ...uiState.classes, [className]: true }), removeClass: className => { const { [className]: removed, ...rest } = uiState.classes; uiState.classes = rest; }, hasClass: className => Boolean(rootClasses.value[className]), setRippleCenter: normalizedX => uiState.lineRippleEl?.setRippleCenter(normalizedX), activateBottomLine: () => uiState.lineRippleEl?.activate(), deactivateBottomLine: () => uiState.lineRippleEl?.deactivate(), notifyChange: value => { const index = foundation.getSelectedIndex(); emitCustomEvent( uiState.root, strings.CHANGE_EVENT, { value, index }, true /* shouldBubble */, ); value != props.modelValue && emit('update:modelValue', value); }, // outline methods hasOutline: () => props.outlined, notchOutline: labelWidth => uiState.outlineEl?.notch(labelWidth), closeOutline: () => uiState.outlineEl?.closeNotch(), // label methods hasLabel: () => !!props.label, floatLabel: shouldFloat => (uiState.labelEl || uiState.outlineEl)?.float(shouldFloat), getLabelWidth: () => { return uiState.labelEl?.getWidth() ?? labelElement?.scrollWidth ?? 0; }, setLabelRequired: isRequired => uiState.labelEl?.setRequired(isRequired), }; const setFixedPosition = isFixed => uiState.menu.setFixedPosition(isFixed); const refreshIndex = () => { const menuItemValues = uiState.menu?.getMenuItemValues( strings.VALUE_ATTR, ); const index = menuItemValues.indexOf(props.modelValue); uiState.menu.setSelectedIndex(index); return index; }; watch( () => props.disabled, nv => foundation?.updateDisabledStyle(nv), ); watch( () => props.modelValue, () => { const index = refreshIndex(); foundation.setSelectedIndex(index); }, ); onMounted(() => { // menu setup uiState.menu.hasTypeahead = true; uiState.menu.setSingleSelection = true; foundation = new MDCSelectFoundation(adapter, { helperText: uiState.helperTextEl?.foundation, leadingIcon: uiState.leadingIconEl?.foundation, }); // initial sync with DOM refreshIndex(); foundation.init(); labelElement = uiState.root.querySelector('.mdc-floating-label'); }); onBeforeUnmount(() => { foundation.destroy(); }); return { ...toRefs(uiState), rootClasses, handleBlur, handleFocus, handleClick, handleChange, handleKeydown, layout, layoutOptions, rippleClasses, rippleStyles, handleMenuItemAction, refreshIndex, setFixedPosition, handleMenuOpened, handleMenuClosed, }; }, components: { SelectHelperText, SelectIcon }, }; // === // Private functions // === function getNormalizedXCoordinate(event_) { const targetClientRect = event_.target.getBoundingClientRect(); const xCoordinate = event_.clientX; return xCoordinate - targetClientRect.left; }