UNPKG

@coreui/vue

Version:

UI Components Library for Vue.js

340 lines (302 loc) 8.64 kB
import { computed, defineComponent, h, ref } from 'vue' import { Color } from '../../props' const SELECTOR_FOCUSABLE_ITEMS = '.chip[tabindex="0"]:not(.disabled)' const CChip = defineComponent({ name: 'CChip', props: { /** * Toggle the active state for the component. */ active: Boolean, /** * Provides an accessible label for the remove button. */ ariaRemoveLabel: { type: String, default: 'Remove', }, /** * Component used for the root node. Either a string to use a HTML element or a component. */ as: { type: String, default: 'span', }, /** * Enables interactive hover styling and pointer cursor. */ clickable: Boolean, /** * Sets the color context of the component to one of CoreUI's themed colors. * * @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light' */ color: Color, /** * Toggle the disabled state for the component. */ disabled: Boolean, /** * Displays a remove button inside the component. */ removable: Boolean, /** * Replaces the default remove icon with a custom icon node. */ removeIcon: { type: [String, Object], default: undefined, }, /** * Enables selectable behavior and keyboard toggle support. */ selectable: Boolean, /** * Controls the selected state of a selectable component. */ selected: Boolean, /** * Size the component small or large. * * @values 'sm', 'lg' */ size: { type: String, validator: (value: string) => { return ['sm', 'lg'].includes(value) }, }, /** * Set the button variant to an outlined style. * * @values 'outline' */ variant: { type: String, validator: (value: string) => { return value === 'outline' }, }, }, emits: [ /** * Event called when the user clicks on the component. */ 'click', /** * Event called when the component becomes deselected. */ 'deselect', /** * Event called when the user presses a key. */ 'keydown', /** * Event called when the component requests removal. */ 'remove', /** * Event called when the component becomes selected. */ 'select', /** * Event called when the selected state changes. */ 'selected-change', ], setup(props, { attrs, emit, slots, expose }) { const chipRef = ref<HTMLElement>() const internalSelected = ref(false) const selectedState = computed(() => props.selected !== undefined ? Boolean(props.selected) : internalSelected.value ) const isFocusable = computed(() => Boolean(!props.disabled && (props.selectable || props.removable))) const getFocusableSibling = (shouldGetNext: boolean): HTMLElement | null => { const currentElement = chipRef.value if (!currentElement?.parentElement) { return null } const chips = Array.from( currentElement.parentElement.querySelectorAll<HTMLElement>(SELECTOR_FOCUSABLE_ITEMS), ) const index = chips.indexOf(currentElement as HTMLElement) if (index === -1 || chips.length <= 1) { return null } const targetIndex = shouldGetNext ? index + 1 : index - 1 return chips[targetIndex] ?? null } const navigateToEdge = (targetIndex: 0 | -1): void => { const currentElement = chipRef.value if (!currentElement?.parentElement) { return } const chips = Array.from( currentElement.parentElement.querySelectorAll<HTMLElement>(SELECTOR_FOCUSABLE_ITEMS), ) const edgeChip = targetIndex === -1 ? chips[chips.length - 1] : chips[0] edgeChip?.focus() } const setSelectableState = ( nextSelected: boolean, event: MouseEvent | KeyboardEvent, ): void => { if (!props.selectable || props.disabled || nextSelected === selectedState.value) { return } if (props.selected === undefined) { internalSelected.value = nextSelected } if (nextSelected) { emit('select', event) } else { emit('deselect', event) } emit('selected-change', nextSelected, event) } const toggleSelectedState = (event: MouseEvent | KeyboardEvent): void => { setSelectableState(!selectedState.value, event) } const handleRemove = (event: MouseEvent | KeyboardEvent): void => { emit('remove', event) } const handleRemoveClick = (event: MouseEvent): void => { event.stopPropagation() handleRemove(event) } const handleClick = (event: MouseEvent): void => { if (props.disabled) { return } if ((event.target as HTMLElement).closest('.chip-remove')) { return } if (props.selectable) { toggleSelectedState(event) } emit('click', event) } const handleKeydown = (event: KeyboardEvent): void => { if (props.disabled) { emit('keydown', event) return } switch (event.key) { case 'Enter': case ' ': case 'Spacebar': { if (props.selectable) { event.preventDefault() toggleSelectedState(event) } break } case 'Backspace': case 'Delete': { if (props.removable) { event.preventDefault() const sibling = getFocusableSibling(false) || getFocusableSibling(true) sibling?.focus() handleRemove(event) } break } case 'ArrowLeft': { event.preventDefault() const sibling = getFocusableSibling(false) sibling?.focus() if (selectedState.value && event.shiftKey) { sibling?.dispatchEvent(new CustomEvent('coreui-chip-select')) } break } case 'ArrowRight': { event.preventDefault() const sibling = getFocusableSibling(true) sibling?.focus() if (selectedState.value && event.shiftKey) { sibling?.dispatchEvent(new CustomEvent('coreui-chip-select')) } break } case 'Home': { event.preventDefault() navigateToEdge(0) break } case 'End': { event.preventDefault() navigateToEdge(-1) break } // No default } emit('keydown', event) } expose({ chipRef }) return () => { const removeIconVNode = slots.removeIcon?.() || props.removeIcon || h( 'svg', { xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap': 'round', }, [ h('line', { x1: 4, y1: 4, x2: 12, y2: 12 }), h('line', { x1: 12, y1: 4, x2: 4, y2: 12 }), ], ) const children = [ slots.default && slots.default(), props.removable && h( 'button', { type: 'button', class: 'chip-remove', 'aria-label': props.ariaRemoveLabel, onClick: handleRemoveClick, tabindex: -1, disabled: props.disabled, }, removeIconVNode, ), ].filter(Boolean) return h( props.as, { ref: chipRef, class: [ 'chip', { active: props.selectable ? selectedState.value : props.active, disabled: props.disabled, [`chip-${props.color}`]: props.color, [`chip-${props.size}`]: props.size, 'chip-clickable': props.clickable || props.selectable || attrs.onClick, 'chip-outline': props.variant === 'outline', }, attrs.class, ], ...(props.disabled && { 'aria-disabled': true }), ...(props.selectable && { 'aria-selected': selectedState.value }), ...(isFocusable.value && !attrs.tabindex && { tabindex: 0 }), onClick: handleClick, onKeydown: handleKeydown, ...(props.as === 'button' && { disabled: props.disabled }), }, children, ) } }, }) export { CChip }