Skip to content

Commit

Permalink
feat(button): add tooltip prop (#255) (#258) (#271)
Browse files Browse the repository at this point in the history
close #255
close #258

---------

Co-authored-by: Kia King Ishii <kia.king.08@gmail.com>
  • Loading branch information
brc-dd and kiaking committed Apr 27, 2023
1 parent cb3b998 commit 5a74398
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 26 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function sidebar(): DefaultTheme.SidebarItem[] {
{ text: 'SAvatar', link: '/components/avatar' },
{ text: 'SButton', link: '/components/button' },
{ text: 'SButtonGroup', link: '/components/button-group' },
{ text: 'SFragment', link: '/components/fragment' },
{ text: 'SInputAddon', link: '/components/input-addon' },
{ text: 'SInputCheckbox', link: '/components/input-checkbox' },
{ text: 'SInputCheckboxes', link: '/components/input-checkboxes' },
Expand Down
59 changes: 59 additions & 0 deletions docs/components/button.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,65 @@ interface Props {
<SButton label="Button" disabled />
```

### `:tooltip`

Display a tooltip when the user interacts with the button. Under the hood, this component uses [`STooltip`](tooltip) component.

The tooltip will only be visible when `:tooltip.text` is set.

```ts
interface Props {
tooltip?: {
// The HTML tag to be used for the tooltip.
// Defaults to `span`.
tag?: string

// The text to be displayed in the tooltip. The tooltip
// will only be visible when this prop is set.
text?: MaybeRef<string>

// The position of the tooltip relative to the button.
// Defaults to `top`
position?: 'top' | 'right' | 'bottom' | 'left'

// The trigger to show the tooltip.
// Defaults to `both`
trigger?: 'hover' | 'focus' | 'both'

// Defines the timeout in milliseconds to hide the tooltip.
// Used only when `trigger` is set to `'focus'` or `'both'`.
// Defaults to `undefined` (the tooltip will not hide
// automatically).
timeout?: number
}
}
```

```vue-html
<SButton
label="Button"
:tooltip="{
text: 'Tooltip text'
}"
/>
```

## Slots

Here are the list of slots you may define within the component.

### `@tooltip-text`

The content of tooltip. Same as `:tooltip.text` prop. When `:tooltip.text` prop and this slot are defined at the same time, this slot will take precedence.

```vue-html
<SButton label="Button">
<template #tooltip-text>
Tooltip text
</template>
</SButton>
```

## Events

Here are the list of events the component may emit.
Expand Down
52 changes: 52 additions & 0 deletions docs/components/fragment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# SFragment

`<SFragment>` is a utility component that allows you to wrap some component with another component based on conditions.

## Usage

Import `<SFragment>` component and wrap it around with other component. Specify `:is` prop to define the component to be used. When `:is` prop is not defined, it will render the underlining component directly.

This component is useful in a case for example you want to wrap some component with `<STooltip>` only when some props are passed.

```vue
<script setup lang="ts">
import { ref } from 'vue'
import SButton from '@globalbrain/sefirot/lib/components/SButton.vue'
import STooltip from '@globalbrain/sefirot/lib/components/STooltip.vue'
import SButton from '@globalbrain/sefirot/lib/components/SFragment.vue'
const tooltip = ref('')
</script>
<template>
<!--
Wrap component with `<SToooltip>` only when tooltip ref
is set. Otherwise, it will render `<SButton>` directly.
-->
<SFragment :is="tooltip && STooltip" :text="tooltip">
<SButton label="Button" />
</SFragment>
</template>
```

Note that `<SFragment>` will pass down all additional props defined along with `:is`, for example in the above case `:text` is passed to `<STooltip>`.

## Props

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

### `:is`

The component to be used to wrap the children. When this prop is not defined, it will render the children directly.

```ts
interface Props {
is?: any
}
```

```vue-html
<SFragment :is="STooltip">
...
</SFragment>
```
16 changes: 16 additions & 0 deletions docs/components/tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ interface Props {
</STooltip>
```

### `:display`

Defines the css `display` property of the tooltip. Defaults to undefined.

```ts
interface Props {
display?: 'inline' | 'inline-block' | 'block'
}
```

```vue-html
<STooltip text="..." display="inline-block">
...
</STooltip>
```

### `:trigger`

Defines the trigger event to show the tooltip. Defaults to `'hover'`.
Expand Down
73 changes: 52 additions & 21 deletions lib/components/SButton.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { MaybeRef } from '@vueuse/core'
import { computed, unref, useSlots } from 'vue'
import type { Position } from '../composables/Tooltip'
import SFragment from './SFragment.vue'
import SIcon from './SIcon.vue'
import SLink from './SLink.vue'
import SSpinner from './SSpinner.vue'
import STooltip from './STooltip.vue'
export type Size = 'mini' | 'small' | 'medium' | 'large' | 'jumbo'
Expand All @@ -18,6 +22,14 @@ export type Mode =
| 'warning'
| 'danger'
export interface Tooltip {
tag?: string
text?: MaybeRef<string>
position?: Position
trigger?: 'hover' | 'focus' | 'both'
timeout?: number
}
const props = defineProps<{
tag?: string
size?: Size
Expand All @@ -32,6 +44,7 @@ const props = defineProps<{
block?: boolean
loading?: boolean
disabled?: boolean
tooltip?: Tooltip
}>()
const emit = defineEmits<{
Expand All @@ -57,6 +70,12 @@ const computedTag = computed(() => {
: props.href ? SLink : 'button'
})
const slots = useSlots()
const hasTooltip = computed(() => {
return slots['tooltip-text'] || unref(props.tooltip?.text)
})
function handleClick(): void {
if (!props.loading) {
props.disabled ? emit('disabled-click') : emit('click')
Expand All @@ -65,29 +84,41 @@ function handleClick(): void {
</script>

<template>
<Component
:is="computedTag"
class="SButton"
:class="classes"
:href="href"
role="button"
@click="handleClick"
<SFragment
:is="hasTooltip && STooltip"
:tag="tooltip?.tag"
:text="unref(tooltip?.text)"
:position="tooltip?.position"
display="inline-block"
:trigger="tooltip?.trigger ?? 'both'"
:timeout="tooltip?.timeout"
:tabindex="-1"
>
<span class="content">
<span v-if="icon" class="icon" :class="iconMode">
<SIcon :icon="icon" class="icon-svg" />
<template v-if="$slots['tooltip-text']" #text><slot name="tooltip-text" /></template>
<component
:is="computedTag"
class="SButton"
:class="classes"
:href="href"
role="button"
@click="handleClick"
>
<span class="content">
<span v-if="icon" class="icon" :class="iconMode">
<SIcon :icon="icon" class="icon-svg" />
</span>
<span v-if="label" class="label" :class="labelMode">
{{ label }}
</span>
</span>
<span v-if="label" class="label" :class="labelMode">
{{ label }}
</span>
</span>
<Transition name="fade">
<span v-if="loading" key="loading" class="loader">
<SSpinner class="loader-icon" />
</span>
</Transition>
</Component>
<transition name="fade">
<span v-if="loading" key="loading" class="loader">
<SSpinner class="loader-icon" />
</span>
</transition>
</component>
</SFragment>
</template>
<style scoped lang="postcss">
Expand Down
16 changes: 16 additions & 0 deletions lib/components/SFragment.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
export default { inheritAttrs: false }
</script>

<script setup lang="ts">
defineProps<{ is?: any }>()
</script>

<template>
<component v-if="is" :is="is" v-bind="$attrs">
<template v-for="(_, slot) of $slots" #[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
</component>
<slot v-else />
</template>
23 changes: 19 additions & 4 deletions lib/components/STooltip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const props = withDefaults(defineProps<{
tag?: string
text?: string
position?: Position
display?: 'inline' | 'inline-block' | 'block'
trigger?: 'hover' | 'focus' | 'both'
timeout?: number
tabindex?: number
Expand All @@ -20,7 +21,14 @@ const props = withDefaults(defineProps<{
const el = shallowRef<HTMLElement | null>(null)
const tip = shallowRef<HTMLElement | null>(null)
const content = shallowRef<HTMLElement | null>(null)
const classes = computed(() => [props.position])
const rootClasses = computed(() => [
props.display,
props.tabindex && (props.tabindex > -1) && 'focusable'
])
const containerClasses = computed(() => [props.position])
const timeoutId = ref<number | null>(null)
const tabindex = computed(() => {
Expand Down Expand Up @@ -71,7 +79,10 @@ function onFocus() {
}
function onBlur() {
if (props.trigger === 'focus' || props.trigger === 'both') {
if (
props.trigger === 'focus'
|| (props.trigger === 'both' && !el.value?.matches(':hover'))
) {
hide()
}
}
Expand All @@ -82,7 +93,7 @@ function onBlur() {
ref="el"
:is="tag"
class="STooltip"
:class="tabindex > -1 && 'focusable'"
:class="rootClasses"
:tabindex="tabindex"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
Expand All @@ -94,7 +105,7 @@ function onBlur() {
</span>

<transition name="fade">
<span v-show="on" class="container" :class="classes" ref="tip">
<span v-show="on" class="container" :class="containerClasses" ref="tip">
<span v-if="$slots.text" class="tip"><slot name="text" /></span>
<SMarkdown v-else-if="text" tag="span" class="tip" :content="text" inline />
</span>
Expand All @@ -113,6 +124,10 @@ function onBlur() {
&.focusable {
cursor: pointer;
}
&.inline { display: inline; }
&.inline-block { display: inline-block; }
&.block { display: block; }
}
.content {
Expand Down

0 comments on commit 5a74398

Please sign in to comment.