Skip to content

Instantly share code, notes, and snippets.

@bashunaimiroy
Last active March 26, 2020 17:09
Show Gist options
  • Save bashunaimiroy/566c43c1fdeac56fcb9bfcf339f76029 to your computer and use it in GitHub Desktop.
Save bashunaimiroy/566c43c1fdeac56fcb9bfcf339f76029 to your computer and use it in GitHub Desktop.
Accessible Vue Select
/*
This component renders a Styled Select component (Vue Select from https://vue-select.org/),
and also a native select. The native select is visually hidden until focused. Selecting an option in one
will mirror that selection in the other, so they are kept in sync.
The component willl emit an "input" event when a selection is made.
Note that the emitted event will have the entire option object as its data, not just its "value" property.
Note: The 'valuePropertyName' and 'labelPropertyName' properties let us render both the v-select Options and the native <select> element's option elements according to a single source of truth: the Options prop.
If the Options prop is an array of objects, the v-select component will use the 'label' attribute as the property name to find the Label for each option, and the Option Object as the Value for each option.
The native Select will render its options from properties of the Option Object, using the strings labelPropertyName and valuePropertyName to determine the attributes.
So for example, for a Shopify variant Dropdown, we might pass in the array of variant objects as the Options Prop, and specify a Label attribute 'title' and a valuePropertyName prop 'id'.
In that example, the v-select will render each option using the variant.title as its label and the entire variant object as its value, and the native select will render each option using variant.title as its label and variant.id as its value.
*/
((theme, Vue) => {
Vue.component("accessible-select", {
props: {
options: { type: Array, required: true },
placeholder: { type: String, required: false, default: "" },
modifier: { type: String, required: false, default: "" },
selectId: { type: String, required: true },
valuePropertyName: { type: String, required: false, default: "value" },
name: { type: String, required: false, default: "" }
},
data() {
return {
labelPropertyName: this.$attrs.label || "label"
};
},
computed: {
picked: {
get() {
return this.$attrs.value;
},
set(value) {
this.$attrs.value = value;
this.$emit("input", value);
}
}
},
methods: {
nativePickValue(stringValue) {
// find the option that was picked in the native select by filtering through options
// until we find one where the 'value' property matches the selected "value" string,
// then set this.picked accordingly.
this.picked = this.options.find(
obj => obj[this.valuePropertyName] === stringValue
);
}
},
template: "#js-accessible-select-template"
});
})((window.theme = window.theme || {}), window.Vue);
{%- comment -%}
Vue template containing markup for the select dropdown component
Props:
1. 'modifier' - a string that will be interpolated into a BEM Modifier class. Examples: 'frontpage', 'accessory'
2. 'selectId' - a string that will be used as the ID attribute for the accessible native <select> element. Should match with the 'for' attribute of any label you're using.
3. 'placeholder' - a placeholder for the selects
4. 'Options' - a list of options, expressed as objects with a 'label' and 'value' property.
5. 'valuePropertyName' - a string that will be used to set the value for each native option, by selecting that property on the option Object. Default is 'value'
5. 'name' - a string that will be used for the name attribute when used in forms.
Non-prop attributes:
1. "label" - if the "option" objects you are passing in have a special property that you want to use as the label, pass in the property's name here.
{%- endcomment -%}
<script id='js-accessible-select-template' type='text/x-template'>
<div class="o-select__wrapper">
<!-- stylable vue-select element -->
<v-select aria-hidden="true" class="o-select" v-bind="$attrs" v-bind:class="`o-select--${modifier}`" v-bind:tabindex="-1" v-bind:options='options' v-model="picked" v-bind:placeholder="placeholder" v-bind:clearable="false" v-bind:searchable="false">
</v-select>
<!-- native select element -->
<select class="o-select__native-select u-visually-hidden--unless-focused" v-bind:name="name" v-bind:id="selectId" v-bind:value="picked && picked[valuePropertyName]"
v-on:input="nativePickValue($event.target.value)">
<option selected disabled>${placeholder}</option>
<option v-for="(item,index) in options" v-bind:value="item[valuePropertyName]" v-bind:key="index">${item[labelPropertyName]}</option>
</select>
</div>
</script>
@mixin visuallyHidden {
clip: rect(0 0 0 0);
clip: rect(0, 0, 0, 0);
overflow: hidden;
position: absolute;
height: 1px;
width: 1px;
}
.u-visually-hidden--unless-focused {
&:not(:focus){
@include visuallyHidden;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment