Skip to content

Commit

Permalink
feat(input-addon): add SInputAddon (#202) (#206)
Browse files Browse the repository at this point in the history
close #202

Co-authored-by: Kia Ishii <kia.king.08@gmail.com>
  • Loading branch information
NozomuIkuta and kiaking committed Jan 30, 2023
1 parent 29e84fb commit f6661ed
Show file tree
Hide file tree
Showing 15 changed files with 1,089 additions and 140 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function sidebar() {
items: [
{ text: 'SAvatar', link: '/components/avatar' },
{ text: 'SButton', link: '/components/button' },
{ text: 'SInputAddon', link: '/components/input-addon' },
{ text: 'SInputFile', link: '/components/input-file' },
{ text: 'SInputNumber', link: '/components/input-number' },
{ text: 'SInputRadios', link: '/components/input-radios' },
Expand Down
178 changes: 178 additions & 0 deletions docs/components/input-addon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<script setup lang="ts">
import { ref } from 'vue'
import SInputAddon from 'sefirot/components/SInputAddon.vue'
import SInputText from 'sefirot/components/SInputText.vue'

const input = ref<string | null>(null)
</script>

# SInputAddon

`<SInputAddon>` is a special component that can be used to add extra label or action to other input.

<Showcase
path="/components/SInputText.vue"
story="/stories-components-sinputtext-02-addons-story-vue"
>
<SInputText placeholder="johndoe" v-model="input">
<template #addon-before>
<SInputAddon label="@" :clickable="false" />
</template>
</SInputText>
</Showcase>
## Usage

In order to use `<SInputAddon>`, you must inject it through slots. Currently, the supported components are listed below.

- `<SInputText>`
- [`<SInputNumber>`](/components/input-number)

You may choose the position of the addon to be injected using the slot name either `#addon-before` or `#addon-after`.

```vue
<script setup lang="ts">
import { ref } from 'vue'
import SInputAddon from '@globalbrain/sefirot/lib/components/SInputAddon.vue'
import SInputText from '@globalbrain/sefirot/lib/components/SInputText.vue'
const input = ref<string | null>(null)
</script>
<template>
<SInputNumber placeholder="johndoe" v-model="input">
<!-- Inject addon before the input box. -->
<template #addon-before>
<SInputAddon label="@" />
</template>
<!-- Inject addon after the input box. -->
<template #addon-after>
<SInputAddon label="@" />
</template>
</SInputNumber>
</template>
```

## Props

Here are the list of props you may pass to the component.

### `:label`

Defines the label of the addon button.

```ts
import { IconifyIcon } from '@iconify/vue/dist/offline'

interface Props {
label?: string | IconifyIcon
}
```

```vue-html
<SInputAddon label="@" />
```

### `:clickable`

Defines whether the button should be clickable. Defaults to `true`.

```ts
interface Props {
clickable?: boolean
}
```

```vue-html
<SInputAddon label="@" :clickable="false" />
```

### `:dropdown`

Defines dropdown option. When this prop is set, the dropdown will pop up when user clicks the addon button. In addition, if `:label` is not defined, the selected option's label will be used for label value.

```ts
import { DropdownSection } from '@globalbrain/sefirot/lib/composables/Dropdown'

interface Props {
dropdown?: DropdownSection[]
}
```

```vue
<script setup lang="ts">
import { createDropdown } from '@globalbrain/sefirot/lib/composables/Dropdown'
const dropdown = createDropdown([
{
type: 'filter',
selected: 1,
options: [
{ label: 'Item 01', value: 1, onClick: () => {} },
{ label: 'Item 02', value: 2, onClick: () => {} }
]
}
])
</script>
<template>
<SInputAddon :dropdown="dropdown" />
</template>
```

### `:dropdown-caret`

Whether to show caret icon when `:dropdown` is defined. Defaults to `true`.

```ts
interface Props {
dropdownCaret?: boolean
}
```

```vue-html
<SInputAddon :dropdown="dropdown" :dropdown-caret="false" />
```

### `:dropdown-position`

Fix the dropdown dialog position. If it's not defined, the dialog will be placed based on window space.

```ts
interface Props {
dropdowpPosition?: 'top' | 'bottom'
}
```

```vue-html
<SInputAddon :dropdown="dropdown" :dropdown-position="bottom" />
```

### `:disabled`

Disable the addon action. When this prop is set, no events are emitted.

```ts
interface Props {
disabled?: boolean
}
```

```vue-html
<SInputAddon label="Button" disabled />
```

## Events

Here are the list of events the component may emit.

### `@click`

Emits when the user clicks the addon button. It will not be emitted when `:clickable` is set to `false`, or `:disabled` is set.

```ts
interface Emits {
(e: 'click'): void
}
```
24 changes: 24 additions & 0 deletions docs/components/input-number.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,30 @@ Same as `info` prop. When `info` prop and this slot are defined at the same time
</SInputNumber>
```

### `#addon-before`

Inject [`<SInputAddon>`](/components/input-addon) before the input. Learn more details about addon in [Components: SInputAddon](/components/input-addon).

```vue-html
<SInputNumber label="..." v-model="...">
<template #addon-before>
<SInputAddon label="@" />
</template>
</SInputNumber>
```

### `#addon-after`

Inject [`<SInputAddon>`](/components/input-addon) after the input. Learn more details about addon in [Components: SInputAddon](/components/input-addon).

```vue-html
<SInputNumber label="..." v-model="...">
<template #addon-after>
<SInputAddon label="@" />
</template>
</SInputNumber>
```

## Events

Here are the list of events the component may emit.
Expand Down
143 changes: 143 additions & 0 deletions lib/components/SInputAddon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<script setup lang="ts">
import IconCaretDown from '@iconify-icons/ph/caret-down-bold'
import {
DropdownSection,
getSelectedOption,
useManualDropdownPosition
} from 'sefirot/composables/Dropdown'
import { useFlyout } from 'sefirot/composables/Flyout'
import { computed, ref } from 'vue'
import { isString } from '../support/Utils'
import SDropdown from './SDropdown.vue'
import SIcon from './SIcon.vue'
const props = withDefaults(defineProps<{
label?: any
clickable?: boolean
dropdown?: DropdownSection[]
dropdownCaret?: boolean
dropdowpPosition?: 'top' | 'bottom'
disabled?: boolean
}>(), {
clickable: true,
dropdown: () => [],
dropdownCaret: true
})
const emit = defineEmits<{
(e: 'click'): void
}>()
const container = ref<any>(null)
const isFocused = ref(false)
const classes = computed(() => [
{ clickable: props.clickable },
{ focused: isFocused.value },
{ disabled: props.disabled }
])
const selectedOptionLabel = computed(() => {
return getSelectedOption(props.dropdown)?.label ?? null
})
const { isOpen, open } = useFlyout(container)
const { position, update: updatePosition } = useManualDropdownPosition(container)
function handleFocus() {
if (!props.disabled) {
isFocused.value = true
}
}
function handleBlur() {
if (!props.disabled) {
isFocused.value = false
}
}
function handleClickButton() {
if (!props.disabled) {
emit('click')
if (props.dropdown.length) {
updatePosition()
open()
}
}
}
</script>

<template>
<div class="SInputAddon" :class="classes" ref="container" @click.stop>
<component
:is="clickable ? 'button' : 'div'"
class="action"
:disabled="clickable ? props.disabled : null"
@focus="handleFocus"
@blur="handleBlur"
@click="handleClickButton"
>
<span class="action-label">
<SIcon
v-if="props.label && !isString(props.label)"
class="action-icon"
:icon="props.label"
/>
<span v-else>
{{ props.label ?? selectedOptionLabel }}
</span>
</span>

<SIcon
v-if="props.dropdown.length && props.dropdownCaret"
class="caret"
:icon="IconCaretDown"
/>
</component>

<div v-if="isOpen" class="dialog" :class="position">
<SDropdown :sections="dropdown" />
</div>
</div>
</template>

<style scoped lang="postcss">
.SInputAddon {
position: relative;
}

.action {
display: flex;
align-items: center;
height: 100%;
background-color: var(--button-fill-mute-bg-color);
transition: background-color 0.25s;

.SInputAddon.clickable &:hover,
.SInputAddon.clickable.focused & {
background-color: var(--button-fill-mute-hover-bg-color);
}

.SInputAddon.clickable &:active {
background-color: var(--button-fill-mute-active-bg-color);
}

.SInputAddon.disabled &,
.SInputAddon.disabled.clickable &:hover,
.SInputAddon.disabled.clickable &:active,
.SInputAddon.disabled.clickable.focused & {
background-color: var(--button-fill-mute-bg-color);
cursor: not-allowed;
}
}

.dialog {
position: absolute;
z-index: var(--z-index-dropdown);

&.top { bottom: calc(100% + 8px); }
&.bottom { top: calc(100% + 8px); }
}
</style>

0 comments on commit f6661ed

Please sign in to comment.