All files / components/toggle-button-group ToggleButtonGroup.vue

100% Statements 39/39
93.54% Branches 29/31
100% Functions 11/11
100% Lines 39/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163    1x 2x       1x     1x 1x 1x 1x         1x                           28x                                                                         21x   336x 180x 156x 108x   48x     7x 4x 4x   2x 2x   2x 4x           3x 2x             21x             1x 1x 1x 1x 1x   28x 28x 28x   112x         7x   224x         216x                             1x 1x                                
<template>
	<div class="cdx-toggle-button-group">
		<cdx-toggle-button
			v-for="button in buttons"
			:key="button.value"
			:model-value="isSelected( button )"
			:disabled="button.disabled || disabled"
			:aria-label="button.ariaLabel"
			@update:model-value="onUpdate( button, $event )"
		>
			<!--
				@slot Content of an individual button
				@binding {ButtonGroupItem} button Object describing the button to display
				@binding {boolean} selected Whether the button is selected
			-->
			<slot :button="button" :selected="isSelected( button )">
				<cdx-icon v-if="button.icon" :icon="button.icon" />
				{{ getButtonLabel( button ) }}
			</slot>
		</cdx-toggle-button>
	</div>
</template>
 
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { ButtonGroupItem } from '../../types';
import { getButtonLabel } from '../../utils/buttonHelpers';
import CdxIcon from '../icon/Icon.vue';
import CdxToggleButton from '../toggle-button/ToggleButton.vue';
 
/**
 * A group of ToggleButtons. May be configured to allow for
 * either single-select or multi-select behavior.
 */
export default defineComponent( {
	name: 'CdxToggleButtonGroup',
	components: {
		CdxIcon,
		CdxToggleButton
	},
	props: {
		/**
		 * Buttons to display. See the ButtonGroupItem type.
		 */
		buttons: {
			type: Array as PropType<ButtonGroupItem[]>,
			required: true,
			validator: ( value: unknown ) => Array.isArray( value ) && value.length >= 1
		},
		/**
		 * Selected value, or array of selected values.
		 *
		 * If this is a string or number, the button whose value equals that string or number is
		 * selected, and only a single selection is allowed. If this is an array, the buttons whose
		 * values equal any of the values in the array are selected, and multiple selections are
		 * allowed. To select none of the buttons initially, set this to `null`
		 * (for single-selection groups) or to `[]` (for multi-selection groups).
		 *
		 * Must be bound with `v-model`.
		 */
		modelValue: {
			type: [ String, Number, null, Array ] as
				PropType<string | number | null | ( string | number )[]>,
			required: true
		},
		/**
		 * Whether the entire group is disabled.
		 *
		 * If this is set to true, all buttons in the group are disabled. Buttons can also be
		 * disabled individually by setting their `disabled` property to true.
		 */
		disabled: {
			type: Boolean,
			default: false
		}
	},
	emits: [
		/**
		 * Emitted when modelValue changes.
		 *
		 * @property {string | number | ( string | number )[]} modelValue The new model value
		 */
		'update:modelValue'
	],
	setup( props, E{ emit } ) {
		function isSelected( button: ButtonGroupItem ): boolean {
			if ( Array.isArray( props.modelValue ) ) {
				return props.modelValue.indexOf( button.value ) !== -1;
			} else if ( props.modelValue !== null ) {
				return props.modelValue === button.value;
			}
			return false;
		}
 
		function onUpdate( button: ButtonGroupItem, nowSelected: boolean ) {
			if ( Array.isArray( props.modelValue ) ) {
				const wasSelected = props.modelValue.indexOf( button.value ) !== -1;
				if ( nowSelected && !wasSelected ) {
					// Add button.value to props.modelValue
					emit( 'update:modelValue', props.modelValue.concat( button.value ) );
				} else if ( !nowSelected && wasSelected ) {
					// Remove button.value from props.modelValue
					emit( 'update:modelValue', props.modelValue.filter( ( v ) => v !== button.value ) );
				}
				// The other combinations (nowSelected && wasSelected, !nowSelected && !wasSelected)
				// should be impossible, and updating modelValue is not needed in those cases anyway
			} else {
				if ( nowSelected && props.modelValue !== button.value ) {
					emit( 'update:modelValue', button.value );
				}
				// if !nowSelected, the user has clicked the selected button in a single-select
				// ToggleButtonGroup. This should not lead to the button being unselected
				// (just like with a radio button), so do nothing.
			}
		}
 
		return {
			getButtonLabel,
			isSelected,
			onUpdate
		};
	}
} );
</script>
 
<style lang="less">
@import ( reference ) '@wikimedia/codex-design-tokens/theme-wikimedia-ui.less';
@import ( reference ) '../../themes/mixins/button-group.less';
 
.cdx-toggle-button-group {
	.cdx-mixin-button-group();
 
	.cdx-toggle-button {
		.cdx-mixin-button-group-button();
 
		&--toggled-on:enabled {
			// Make the borders of a toggled-on button appear above the borders of adjacent
			// toggled-off buttons, but not above the borders of an active or focused button
			// (those get `z-index: 3;` in 'button-group.less').
			z-index: @z-index-stacking-2;
 
			/* stylelint-disable stylistic/declaration-colon-newline-after,
				stylistic/value-list-comma-newline-after */
			// When two toggled-on buttons are adjacent to each other, display a white line
			// between them, using the same box-shadow trick as in 'button-group.less'.
			box-shadow: @box-shadow-outset-small-top @box-shadow-color-inverted,
				@box-shadow-outset-small-start @box-shadow-color-inverted;
 
			&:focus {
				// Add a white box-shadow around the existing box-shadow applied to focused
				// ToggleButtons. This is similar to the box-shadow above but slightly different:
				// it appears on all four edges, not just the left and top edges.
				box-shadow: @box-shadow-inset-small @box-shadow-color-progressive--focus,
					@box-shadow-inset-medium @box-shadow-color-inverted,
					@box-shadow-outset-small @box-shadow-color-inverted;
			}
			/* stylelint-enable stylistic/declaration-colon-newline-after,
				stylistic/value-list-comma-newline-after */
		}
	}
}
</style>