diff --git a/.eslintrc.js b/.eslintrc.js index ee636af4cb..1c40080853 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,6 +46,10 @@ module.exports = { }, ], 'vuejs-accessibility/aria-role': 'warn', + 'vuejs-accessibility/label-has-for': [ + 'warn', + { required: { some: ['nesting', 'id'] } }, + ], 'unicorn/filename-case': ['error', { case: 'kebabCase' }], }, settings: { diff --git a/.storybook/decorators/with-rtl.js b/.storybook/decorators/with-rtl.js index 6f1f8f8af7..2ce8ae547f 100644 --- a/.storybook/decorators/with-rtl.js +++ b/.storybook/decorators/with-rtl.js @@ -1,6 +1,6 @@ import Vue from 'vue' -import { ref, watch, useContext } from '@nuxtjs/composition-api' +import { ref, watch, useContext, onMounted } from '@nuxtjs/composition-api' import { useEffect } from '@storybook/client-api' const languageDirection = Vue.observable({ value: 'ltr' }) @@ -16,14 +16,20 @@ export const WithRTL = (story, context) => { setup() { const element = ref() const { i18n } = useContext() - watch(languageDirection, (direction) => { - i18n.localeProperties.dir = direction.value - if (element.value) { - element.value.ownerDocument.documentElement.setAttribute( - 'dir', - direction.value - ) - } + onMounted(() => { + watch( + languageDirection, + (direction) => { + i18n.localeProperties.dir = direction.value + if (element.value) { + element.value.ownerDocument.documentElement.setAttribute( + 'dir', + direction?.value ?? 'ltr' + ) + } + }, + { immediate: true } + ) }) return { element } }, diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg new file mode 100644 index 0000000000..ee371a205f --- /dev/null +++ b/src/assets/icons/search.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/Header/SearchBar/SearchBar.vue b/src/components/Header/SearchBar/SearchBar.vue new file mode 100644 index 0000000000..fd9b7044ca --- /dev/null +++ b/src/components/Header/SearchBar/SearchBar.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/components/Header/SearchBar/SearchButton.vue b/src/components/Header/SearchBar/SearchButton.vue new file mode 100644 index 0000000000..5eb918e57b --- /dev/null +++ b/src/components/Header/SearchBar/SearchButton.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/components/Header/SearchBar/meta/SearchBar.stories.mdx b/src/components/Header/SearchBar/meta/SearchBar.stories.mdx new file mode 100644 index 0000000000..1851645ad0 --- /dev/null +++ b/src/components/Header/SearchBar/meta/SearchBar.stories.mdx @@ -0,0 +1,90 @@ +import { + ArgsTable, + Canvas, + Description, + Meta, + Story, +} from '@storybook/addon-docs' +import SearchBar from '~/components/Header/SearchBar/SearchBar.vue' + + + +export const Template = (args, { argTypes }) => ({ + template: ` + + 12,345 results + `, + components: { SearchBar }, + props: Object.keys(argTypes), +}) + +# Search bar + + + + + +The component emits an `input` event with the new contents of the field whenever +the field receives an input. It also emits the `search` event when the search +button is clicked. + + + + {Template.bind({})} + + + +The recommended way to use it is with `v-model` mapping to a `String` +representing the search query. + +export const vModelTemplate = () => ({ + template: ` +
+ + {{ text.length }} + + {{ text }} +
+ `, + components: { SearchBar }, + data() { + return { + text: 'Hello, World!', + } + }, +}) + + + {vModelTemplate.bind({})} + + +The `SearchBar` component passes all attributes down to the inner `InputField` +which itself applies all its attributes to its inner `` element. So it's +easy `` attributes like placeholders or HTML validations. + + + + {Template.bind({})} + + diff --git a/src/components/InputField/InputField.vue b/src/components/InputField/InputField.vue new file mode 100644 index 0000000000..21b574e468 --- /dev/null +++ b/src/components/InputField/InputField.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/InputField/meta/InputField.stories.mdx b/src/components/InputField/meta/InputField.stories.mdx new file mode 100644 index 0000000000..3c893541f8 --- /dev/null +++ b/src/components/InputField/meta/InputField.stories.mdx @@ -0,0 +1,136 @@ +import { + ArgsTable, + Canvas, + Description, + Meta, + Story, +} from '@storybook/addon-docs' + +import InputField from '~/components/InputField/InputField.vue' + + + +export const Template = (args, { argTypes }) => ({ + template: ` + + Extra info + + `, + components: { InputField }, + props: Object.keys(argTypes), +}) + +# Input field + + + + + +The component emits an `input` event with the new contents of the field whenever +the field receives an input. + + + + {Template.bind({})} + + + +The recommended way to use it is with `v-model` mapping to a `String`. + +export const vModelTemplate = () => ({ + template: ` +
+ + {{ text.length }} + + {{ text }} +
+ `, + components: { InputField }, + data() { + return { + text: 'Hello, World!', + } + }, +}) + + + {vModelTemplate.bind({})} + + +The component is a transparent wrapper over `` so all attributes of the +input element can be applied to it, e.g. `placeholder`. It is recommended not to +change the type as the field is specifically designed for text. + + + + {Template.bind({})} + + + +For a11y, it is recommended that the input field be associated with a label. For +this reason, the field is rendered with an SR-only label. The text can be set +via the `labelText` prop. + + + + {Template.bind({})} + + + +To provide a custom label, skip the `labelText` prop (to remove the SR-only +label) and set the `id` attribute on the component (which is passed to the +`` element). Now use the ID as the `for` attribute of your custom +`