Skip to content

Commit

Permalink
feat(split-button): add split button
Browse files Browse the repository at this point in the history
  • Loading branch information
santiagoballadares committed Feb 20, 2024
1 parent 08b3b9c commit 18e034c
Show file tree
Hide file tree
Showing 8 changed files with 844 additions and 169 deletions.
644 changes: 475 additions & 169 deletions packages/documentation/pages/usage/components/button.vue

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/kotti-ui/source/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export const ISO8601_SECONDS = 'YYYY-MM-DD HH:mm:ss' as const
export const ONE_HUNDRED_PERCENT = 100 as const

export const TIPPY_LIGHT_BORDER_ARROW_HEIGHT = 7

export const TIPPY_VERTICAL_OFFSET = 4
3 changes: 3 additions & 0 deletions packages/kotti-ui/source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ import { KtPopover } from './kotti-popover'
export * from './kotti-popover'
import { KtRow } from './kotti-row'
export * from './kotti-row'
import { KtSplitButton } from './kotti-split-button'
export * from './kotti-split-button'
import {
KtTable,
KtTableColumn,
Expand Down Expand Up @@ -157,6 +159,7 @@ export default {
KtPagination,
KtPopover,
KtRow,
KtSplitButton,
KtTable,
KtTableColumn,
KtTableConsumer,
Expand Down
114 changes: 114 additions & 0 deletions packages/kotti-ui/source/kotti-split-button/KtSplitButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<template>
<div class="kt-split-button">
<KtButton
class="kt-split-button__primary-action"
v-bind="primaryActionButtonProps"
@click="onClickPrimaryAction"
/>
<div ref="tippyTriggerRef">
<KtButton
:class="secondaryActionsButtonClasses"
v-bind="secondaryActionsButtonProps"
@click.stop="onClickSecondaryActions"
/>
</div>
<div ref="tippyContentRef">
<ActionsItem
v-for="(action, index) in actions"
:key="index"
v-bind="{
...action,
index,
rootDataTest: dataTest,
}"
@click="onClickAction"
/>
</div>
</div>
</template>

<script lang="ts">
import { Yoco } from '@3yourmind/yoco'
import omit from 'lodash/omit'
import { computed, defineComponent } from 'vue'
import { makeProps } from '../make-props'
import ActionsItem from './components/ActionsItem.vue'
import { useActionsTippy } from './hooks/use-actions-tippy'
import { KottiSplitButton } from './types'
export default defineComponent({
name: 'KtSplitButton',
components: {
ActionsItem,
},
props: makeProps(KottiSplitButton.propsSchema),
setup(props, { emit }) {
const { isTippyOpen, setIsTippyOpen, tippyContentRef, tippyTriggerRef } =
useActionsTippy({
isDisabled: computed(() => props.isDisabled),
isLoading: computed(() => props.isLoading),
})
return {
onClickAction: () => setIsTippyOpen(false),
onClickPrimaryAction: () => {
if (props.isDisabled || props.isLoading) return
emit('click')
},
onClickSecondaryActions: () => setIsTippyOpen(!isTippyOpen.value),
primaryActionButtonProps: computed(() => ({
...omit(props, ['actions', 'dataTest', 'isDisabled']),
'data-test': [props.dataTest, 'primary-action']
.filter(Boolean)
.join('.'),
disabled: props.isDisabled,
})),
secondaryActionsButtonClasses: computed(() => ({
'kt-split-button__secondary-actions': true,
'kt-split-button__secondary-actions--is-active': isTippyOpen.value,
})),
secondaryActionsButtonProps: computed(() => ({
'data-test': [props.dataTest, 'secondary-actions']
.filter(Boolean)
.join('.'),
disabled: props.isDisabled,
icon: Yoco.Icon.CHEVRON_DOWN,
size: props.size,
type: props.type,
})),
tippyContentRef,
tippyTriggerRef,
}
},
})
</script>

<style lang="scss" scoped>
.kt-split-button {
display: -webkit-inline-box;
&__primary-action {
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
&__secondary-actions {
width: var(--unit-6);
border-left: 0;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
&--is-active {
&.kt-button--type-default:not(:hover) {
background-color: var(--primary-20);
border-color: var(--primary-20);
}
&.kt-button--type-primary:not(:hover) {
background-color: var(--primary-80);
border-color: var(--primary-80);
}
}
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<template>
<button v-bind="itemProps" @click.stop="onItemClick">
<i v-if="icon" class="yoco" v-text="icon" />
<span v-text="label" />
</button>
</template>

<script lang="ts">
import { computed, defineComponent } from 'vue'
import { z } from 'zod'
import { makeProps } from '../../make-props'
import { KottiSplitButton } from '../types'
const propsSchema = KottiSplitButton.actionSchema.extend({
index: z.number(),
rootDataTest: z.string().nullable().default(null),
})
export default defineComponent({
name: 'SplitButtonActionsItem',
props: makeProps(propsSchema),
setup(props, { emit }) {
return {
itemProps: computed(() => ({
class: {
'kt-split-button-actions-item': true,
'kt-split-button-actions-item--is-disabled': props.isDisabled,
},
'data-test': [
props.rootDataTest,
`action-${props.index}`,
props.dataTest,
]
.filter(Boolean)
.join('.'),
disabled: props.isDisabled,
})),
onItemClick: () => {
if (props.isDisabled) return
emit('click')
props.onClick()
},
}
},
})
</script>

<style lang="scss" scoped>
// Remove default styles from <button /> elements
@mixin remove-button-styles {
padding: 0;
margin: 0;
font: inherit;
text-align: inherit;
appearance: none;
background-color: transparent;
border: none;
border-radius: 0;
}
.kt-split-button-actions-item {
@include remove-button-styles;
display: flex;
gap: var(--unit-2);
align-items: center;
width: 100%;
padding: var(--unit-2) var(--unit-4);
color: var(--text-01);
cursor: pointer;
&--is-disabled {
cursor: not-allowed;
opacity: 0.46;
}
&:focus {
outline: none;
}
&:focus,
&:hover:not(.kt-split-button-actions-item--is-disabled) {
background-color: var(--ui-01);
}
.yoco {
font-size: 1rem;
}
span {
font-size: 16px;
line-height: 20px;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useTippy } from '@3yourmind/vue-use-tippy'
import { Instance } from 'tippy.js'
import { Ref, computed, ref, watch } from 'vue'

import { TIPPY_VERTICAL_OFFSET } from '../../constants'

export const useActionsTippy = ({
isDisabled,
isLoading,
}: {
isDisabled: Ref<boolean>
isLoading: Ref<boolean>
}) => {
const isTippyOpen = ref(false)
const tippyContentRef = ref<HTMLDivElement | null>(null)
const tippyInstanceRef = ref<Instance | null>(null)
const tippyTriggerRef = ref<HTMLDivElement | null>(null)

useTippy(
tippyTriggerRef,
computed(() => ({
appendTo: () => document.body,
arrow: false,
content: tippyContentRef.value ?? undefined,
hideOnClick: false,
interactive: true,
offset: [0, TIPPY_VERTICAL_OFFSET],
onClickOutside: () => {
setIsTippyOpen(false)
},
onCreate(instance: Instance) {
tippyInstanceRef.value = instance
},
onHide: () => {
isTippyOpen.value = false
},
onShow: () => {
if (isDisabled.value || isLoading.value) return false
isTippyOpen.value = true
},
placement: 'bottom-end',
theme: 'light-border',
trigger: 'manual',
})),
)

const setIsTippyOpen = (isOpen: boolean) => {
if (!tippyInstanceRef.value) return

if (isOpen) tippyInstanceRef.value.show()
else tippyInstanceRef.value.hide()
}

watch(isTippyOpen, (isOpen) => {
if (!tippyInstanceRef.value || !isOpen) return

const tippyEl = document.getElementById(
`tippy-${tippyInstanceRef.value.id}`,
)

if (tippyEl) {
const containerEl = tippyEl.getElementsByClassName('tippy-content')[0]
containerEl?.setAttribute('style', 'padding: 8px')

const actionsItems = tippyEl.getElementsByClassName(
'kt-split-button-actions-item',
) as HTMLCollectionOf<HTMLButtonElement>

actionsItems[0]?.focus({ preventScroll: true })
}
})

return {
isTippyOpen,
setIsTippyOpen,
tippyContentRef,
tippyTriggerRef,
}
}
24 changes: 24 additions & 0 deletions packages/kotti-ui/source/kotti-split-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MetaDesignType } from '../types/kotti'
import { attachMeta, makeInstallable } from '../utilities'

import KtSplitButtonVue from './KtSplitButton.vue'
import { KottiSplitButton } from './types'

export const KtSplitButton = attachMeta(makeInstallable(KtSplitButtonVue), {
addedVersion: '5.7.0',
deprecated: null,
designs: {
type: MetaDesignType.FIGMA,
url: 'https://www.figma.com/file/0yFVivSWXgFf2ddEF92zkf/Kotti-Design-System?type=design&node-id=5586-8691&mode=design&t=x2i9HCwj1BucUZrh-0',
},
slots: {
default: {
description: 'Used to replace label with custom HTML',
scope: null,
},
},
typeScript: {
namespace: 'Kotti.SplitButton',
schema: KottiSplitButton.propsSchema,
},
})
51 changes: 51 additions & 0 deletions packages/kotti-ui/source/kotti-split-button/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { yocoIconSchema } from '@3yourmind/yoco'
import { z } from 'zod'

import { KottiButton } from '../kotti-button/types'
import { createLooseZodEnumSchema } from '../zod-utilities/enums'

export module KottiSplitButton {
export enum Type {
DEFAULT = KottiButton.Type.DEFAULT,
PRIMARY = KottiButton.Type.PRIMARY,
}
export const typeSchema = createLooseZodEnumSchema(Type)

export const Size = {
...KottiButton.Size,
}
export const sizeSchema = createLooseZodEnumSchema(Size)

export const IconPosition = {
...KottiButton.IconPosition,
}
export const iconPosition = createLooseZodEnumSchema(IconPosition)

export const actionSchema = z.object({
dataTest: z.string().optional(),
icon: yocoIconSchema.nullable().default(null),
isDisabled: z.boolean().default(false),
label: z.string(),
onClick: z.function(z.tuple([]), z.void()),
})
export type Action = z.output<typeof actionSchema>

export const propsSchema = KottiButton.propsSchema
.pick({
icon: true,
isLoading: true,
isSubmit: true,
})
.extend({
actions: z.array(actionSchema).default(() => []),
dataTest: z.string().optional(),
iconPosition: iconPosition.default(IconPosition.LEFT),
isDisabled: z.boolean().default(false),
label: z.string(),
size: sizeSchema.default(Size.MEDIUM),
type: typeSchema.default(Type.DEFAULT),
})

export type Props = z.input<typeof propsSchema>
export type PropsInternal = z.output<typeof propsSchema>
}

0 comments on commit 18e034c

Please sign in to comment.