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.
+
+
+
+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!',
+ }
+ },
+})
+
+
+
+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.
+
+
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.
+
+
+
+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!',
+ }
+ },
+})
+
+
+
+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.
+
+
+
+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.
+
+
+
+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
+`