Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Create the InputField and SearchBar components #380

Merged
merged 38 commits into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3ae1f2a
Add all dark-charcoal colors from styleguide
sarayourfriend Oct 27, 2021
c0d9876
Add base button component
sarayourfriend Oct 27, 2021
4ca2feb
Fix event handling
sarayourfriend Oct 27, 2021
a34763d
Fix typo
sarayourfriend Oct 28, 2021
915c02c
Add large size and fix tertiary-active style
sarayourfriend Oct 28, 2021
72c9500
Add one more variant to cover menu buttons and rename active to pressed
sarayourfriend Oct 28, 2021
aa36b88
Add a11y enhancements to the VButton component
sarayourfriend Oct 29, 2021
5571c16
Correctly handle button input types
sarayourfriend Oct 29, 2021
4b2494a
Run code to set direction on mount
dhruvkb Oct 28, 2021
66e4f50
Add magnifier icon for search
dhruvkb Oct 28, 2021
8c655d6
Set a zero width for ring to hide it
dhruvkb Oct 28, 2021
eb90e67
Create an `InputField` component
dhruvkb Oct 28, 2021
dbf59de
Add start and end margins to the input and extra text
dhruvkb Nov 3, 2021
734a73f
Update the component to match the new designs with individual focus o…
dhruvkb Nov 3, 2021
b7fef48
Allow changing type with a fallback to 'text'
dhruvkb Nov 3, 2021
b65f224
Create the presentational `SearchBar` component
dhruvkb Nov 3, 2021
b768fc3
Change the form's behaviour to use submission as the event trigger
dhruvkb Nov 4, 2021
ea8fe74
Pass attributes down to the `InputField`
dhruvkb Nov 4, 2021
9175908
Add specs for the input field
dhruvkb Nov 4, 2021
cce3148
Add SR text and ARIA label
dhruvkb Nov 4, 2021
4582019
Add specs for the `SearchBar` component
dhruvkb Nov 4, 2021
34a0053
Change extra text to a `slot` to enable advanced markup
dhruvkb Nov 4, 2021
e081937
Merge branch 'main' into input_field
dhruvkb Nov 4, 2021
f06e44e
Replace `onMounted` with `immediate` flag
dhruvkb Nov 9, 2021
a7d1e98
Selectively remove cross from search input field
dhruvkb Nov 9, 2021
b34a843
Make the input field labelled by default, with option to attach more
dhruvkb Nov 10, 2021
c7f8032
Set the placeholder color to match Figma specification
dhruvkb Nov 10, 2021
ef73c50
Use `group-hover` to style search field and button together on hover
dhruvkb Nov 10, 2021
0d9cbce
Merge branch 'main' of https://github.com/WordPress/openverse-fronten…
dhruvkb Nov 10, 2021
894d43d
Merge branch 'main' of https://github.com/WordPress/openverse-fronten…
dhruvkb Nov 11, 2021
be2b67f
Move `watch` inside `onMounted`
dhruvkb Nov 11, 2021
616400a
Make the focus style identical to hover
dhruvkb Nov 12, 2021
cc1ab67
Replace `hover` & `focus` combo with `active` state
dhruvkb Nov 12, 2021
447a973
Change a11y requirement to allow at least one of nesting and ID
dhruvkb Nov 17, 2021
61687b6
Make `fieldId` and `labelText` required props
dhruvkb Nov 17, 2021
b8e9084
Fix bug in field width
dhruvkb Nov 17, 2021
76a97e1
Propagate listeners to nested `<input>`
dhruvkb Nov 17, 2021
5e6dc7d
Update specs to match component changes
dhruvkb Nov 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
24 changes: 15 additions & 9 deletions .storybook/decorators/with-rtl.js
Original file line number Diff line number Diff line change
@@ -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' })
Expand All @@ -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 }
},
Expand Down
6 changes: 6 additions & 0 deletions src/assets/icons/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions src/components/Header/SearchBar/SearchBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>
<form class="search-bar group flex flex-row" @submit.prevent="handleSearch">
<InputField
v-model="text"
v-bind="$attrs"
class="flex-grow search-field"
:connection-sides="['end']"
input-id="search-bar"
type="search"
name="q"
>
<!-- @slot Extra information such as loading message or result count goes here. -->
<slot />
</InputField>
<SearchButton type="submit" />
</form>
</template>

<script>
import { computed } from '@nuxtjs/composition-api'

import InputField from '~/components/InputField/InputField.vue'
import SearchButton from '~/components/Header/SearchBar/SearchButton.vue'

/**
* Displays a text field for a search query and is attached to an action button
* that fires a search request. The loading state and number of hits are also
* displayed in the bar itself.
*/
export default {
name: 'SearchBar',
components: {
InputField,
SearchButton,
},
inheritAttrs: false,
model: {
prop: 'value',
event: 'input',
},
props: {
/**
* the search query given as input to the field
*/
value: {
type: String,
default: '',
},
},
setup(props, { emit }) {
const text = computed({
get() {
return props.value
},
set(value) {
emit('input', value)
},
})

const handleSearch = () => {
emit('submit')
}

return {
text,

handleSearch,
}
},
}
</script>

<style>
/* Removes the cross icon to clear the field */
obulat marked this conversation as resolved.
Show resolved Hide resolved
.search-field input[type='search']::-webkit-search-decoration,
.search-field input[type='search']::-webkit-search-cancel-button,
.search-field input[type='search']::-webkit-search-results-button,
.search-field input[type='search']::-webkit-search-results-decoration {
-webkit-appearance: none;
}
</style>
42 changes: 42 additions & 0 deletions src/components/Header/SearchBar/SearchButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<template>
<button
:type="type"
class="search-button flex items-center justify-center h-12 w-12 hover:text-white group-hover:text-white hover:bg-pink group-hover:bg-pink p-0.5px ps-1.5px focus:p-0 border border-s-0 focus:border-1.5 border-dark-charcoal-20 hover:border-pink group-hover:border-pink focus:border-pink rounded-e-sm focus:outline-none"
:aria-label="$t('search.search')"
v-on="$listeners"
>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<use :href="`${searchIcon}#icon`" />
</svg>
<span class="sr-only">{{ $t('search.search') }}</span>
</button>
</template>

<script>
import searchIcon from '~/assets/icons/search.svg'

export default {
name: 'SearchButton',
setup(props, { attrs }) {
const type = attrs['type'] ?? 'button'

return {
searchIcon,

type,
}
},
}
</script>

<style scoped>
.search-button:active {
box-shadow: 0 0 0 1px inset white;
}
</style>
90 changes: 90 additions & 0 deletions src/components/Header/SearchBar/meta/SearchBar.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
ArgsTable,
Canvas,
Description,
Meta,
Story,
} from '@storybook/addon-docs'
import SearchBar from '~/components/Header/SearchBar/SearchBar.vue'

<Meta
title="Components/Header/Search bar"
component={SearchBar}
argTypes={{
input: {
action: 'input',
},
submit: {
action: 'submit',
},
}}
/>

export const Template = (args, { argTypes }) => ({
template: `
<SearchBar v-bind="$props" v-on="$props">
12,345 results
</SearchBar>`,
components: { SearchBar },
props: Object.keys(argTypes),
})

# Search bar

<Description of={SearchBar} />

<ArgsTable of={SearchBar} />

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.

<Canvas>
<Story
name="Default"
args={{
value: 'Search query',
}}
>
{Template.bind({})}
</Story>
</Canvas>

The recommended way to use it is with `v-model` mapping to a `String`
representing the search query.

export const vModelTemplate = () => ({
template: `
<div>
<SearchBar v-model="text">
{{ text.length }}
</SearchBar>
{{ text }}
</div>
`,
components: { SearchBar },
data() {
return {
text: 'Hello, World!',
}
},
})

<Canvas>
<Story name="v-model">{vModelTemplate.bind({})}</Story>
</Canvas>

The `SearchBar` component passes all attributes down to the inner `InputField`
which itself applies all its attributes to its inner `<input>` element. So it's
easy `<input>` attributes like placeholders or HTML validations.

<Canvas>
<Story
name="With placeholder"
args={{
placeholder: 'Search query',
}}
>
{Template.bind({})}
</Story>
</Canvas>
101 changes: 101 additions & 0 deletions src/components/InputField/InputField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<template>
<div
class="input-field group flex flex-row items-center gap-4 hover:bg-dark-charcoal-06 focus-within:bg-dark-charcoal-06 group-hover:bg-dark-charcoal-06 h-12 p-0.5px focus-within:p-0 border focus-within:border-1.5 border-dark-charcoal-20 rounded-sm overflow-hidden focus-within:border-pink"
:class="[
{
// Padding is set to 1.5px to accommodate the border that will appear later.
'border-s-0 ps-1.5px rounded-s-none': connectionSides.includes('start'),
'border-e-0 pe-1.5px rounded-e-none': connectionSides.includes('end'),
},
]"
>
<label class="sr-only" :for="fieldId">{{ labelText }}</label>
<input
:id="fieldId"
v-model="text"
v-bind="$attrs"
:type="type"
class="flex-grow leading-none font-semibold bg-tx placeholder-dark-charcoal-70 ms-4 h-full focus:outline-none"
v-on="$listeners"
/>
<div
class="info font-semibold text-xs text-dark-charcoal-70 group-hover:text-dark-charcoal group-focus:text-dark-charcoal me-4"
>
<!-- @slot Extra information goes here -->
<slot />
</div>
</div>
</template>

<script>
import { computed } from '@nuxtjs/composition-api'

/**
* Provides a control to enter text as input.
*/
export default {
name: 'InputField',
inheritAttrs: false,
model: {
prop: 'value',
event: 'input',
},
props: {
/**
* the textual content of the input field
*/
value: {
type: String,
default: '',
},
/**
* the textual content of the label associated with this input field; This
* label is SR-only.
*/
labelText: {
type: String,
required: true,
},
/**
* the ID to assign to the field; This can be used to attach custom labels
* to the field.
*/
fieldId: {
type: String,
required: true,
},
/**
* list of sides where the field is connected to other controls
*/
connectionSides: {
type: Array,
default: () => [],
validator: (val) => val.every((item) => ['start', 'end'].includes(item)),
},
},
setup(props, { emit, attrs }) {
const type = attrs['type'] ?? 'text'

const text = computed({
get() {
return props.value
},
set(value) {
emit('input', value)
},
})
dhruvkb marked this conversation as resolved.
Show resolved Hide resolved

return {
type,

text,
}
},
}
</script>

<style scoped>
.input-field:focus-within .info {
@apply text-dark-charcoal;
}
</style>
Loading