diff --git a/.editorconfig b/.editorconfig index 1ed453a371..13fde2a598 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,7 @@ root = true end_of_line = lf insert_final_newline = true -[*.{js,json,yml}] +[*.{js,json,yml,ts,vue}] charset = utf-8 indent_style = space indent_size = 2 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..cb345512fe --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "darkriszty.markdown-table-prettify", + "vue.volar", + "cpylua.language-postcss", + "bradlc.vscode-tailwindcss", + "editorconfig.editorconfig", + "lokalise.i18n-ally" + ] +} diff --git a/components/checkbox/use-checkbox.ts b/components/checkbox/use-checkbox.ts index fbca745eff..ce30329f36 100644 --- a/components/checkbox/use-checkbox.ts +++ b/components/checkbox/use-checkbox.ts @@ -1,6 +1,5 @@ - import { syncRef } from "@vueuse/shared" -import { computed, getCurrentInstance, ref } from "vue-demi" +import { computed, getCurrentInstance, ref, watch } from "vue-demi" import { InputProps } from "../input/use-input" import { valueIn, isEqual } from "../utils/value" @@ -17,30 +16,29 @@ export interface CheckboxProps extends InputProps { uncheckedValue: unknown, } -export function useVModel

(props: P) { +export function useVModel (props: CheckboxProps) { const { emit } = getCurrentInstance() - const localValue = ref(props.checked) - - const checked = props.value ?? true - const unchecked = props.uncheckedValue ?? false + const checked = props.value + const unchecked = props.uncheckedValue + const localValue = ref(isChecked(props.modelValue, checked) || props.checked) const model = computed({ get () { return isChecked(props.modelValue, checked) || props.checked }, set (value: boolean) { - if (!props.readonly && !props.disabled) { - const newValue = value ? checked : unchecked - - if (Array.isArray(props.modelValue)) { - if (value === true) { - if (!valueIn(props.modelValue, newValue)) - emit('update:modelValue', [...props.modelValue, newValue]) - } else - emit('update:modelValue', props.modelValue.filter((old) => !isEqual(old, checked))) + const newValue = value ? checked : unchecked + + if (Array.isArray(props.modelValue)) { + if (value === true) { + if (!valueIn(props.modelValue, newValue)) + emit('update:modelValue', [...props.modelValue, newValue]) } else - emit('update:modelValue', newValue) - } + emit('update:modelValue', props.modelValue.filter((old) => !isEqual(old, checked))) + } else + emit('update:modelValue', newValue) + + emit('change', value) } }) diff --git a/components/input/use-input.ts b/components/input/use-input.ts index 3aa5bbcf07..7d000587f8 100644 --- a/components/input/use-input.ts +++ b/components/input/use-input.ts @@ -1,26 +1,24 @@ import { syncRef } from "@vueuse/shared" -import { computed, getCurrentInstance, ref, Ref } from "vue-demi" +import { computed, getCurrentInstance, ref, Ref, watch } from "vue-demi" export interface InputProps { modelValue: V, - readonly?: boolean, - disabled?: boolean, } export function useVModel(props: InputProps): Ref { - const localValue = ref(props?.modelValue) as Ref + const localValue = ref(props.modelValue) as Ref const { emit } = getCurrentInstance() - const model = computed({ + + const model = computed({ get () { - return localValue.value + return props.modelValue }, - set (newValue) { - if (!props.readonly && !props.disabled) - emit('update:modelValue', newValue) + set (value) { + emit('update:modelValue', value) }, }) syncRef(localValue, model) - return model + return localValue } diff --git a/components/overlay/component.md b/components/overlay/component.md index 536b43352b..8d59abd894 100644 --- a/components/overlay/component.md +++ b/components/overlay/component.md @@ -100,3 +100,6 @@ function onClick () { + +## See Also +- [Spinner](/spinner/component) diff --git a/components/radio/Radio.spec.ts b/components/radio/Radio.spec.ts new file mode 100644 index 0000000000..d7a7308096 --- /dev/null +++ b/components/radio/Radio.spec.ts @@ -0,0 +1,125 @@ +import { fireEvent, render } from "@testing-library/vue" +import { vi } from "vitest" +import { ref } from "vue-demi" +import Radio from "./Radio.vue" + +it('should render properly without any prop', () => { + const screen = render({ + components: { Radio }, + template : ` + + `, + }) + + const radio = screen.getByTestId('radio') + + expect(radio).toBeInTheDocument() + expect(radio).toHaveClass('radio', 'radio--radio') +}) + +it('should checked at start if `checked` props is provided', () => { + const screen = render({ + components: { Radio }, + template : ` + + `, + }) + + const radio = screen.getByTestId('radio') + + expect(radio).toBeInTheDocument() + expect(radio).toHaveClass('radio--checked') +}) + +it('should have readonly style if `readonly` props is provided', () => { + const screen = render({ + components: { Radio }, + template : ` + + `, + }) + + const radio = screen.getByTestId('radio') + + expect(radio).toBeInTheDocument() + expect(radio).toHaveClass('radio--readonly') +}) + +it('should have disabled style if `disabled` props is provided', () => { + const screen = render({ + components: { Radio }, + template : ` + + `, + }) + + const radio = screen.getByTestId('radio') + + expect(radio).toBeInTheDocument() + expect(radio).toHaveClass('radio--disabled') +}) + +it('should have style checkbox if `apperance` set to "checkbox"', () => { + const screen = render({ + components: { Radio }, + template : ` + + `, + }) + + const radio = screen.getByTestId('radio') + + expect(radio).toBeInTheDocument() + expect(radio).toHaveClass('radio--checkbox') + expect(radio).not.toHaveClass('radio--radio') +}) + +it('should modify state in v-model', async () => { + const model = ref() + const screen = render({ + components: { Radio }, + template : ` + Apple + Grape + `, + setup () { + return { + model, + } + } + }) + + const radioApple = screen.queryByText('Apple') + const radioGrape = screen.queryByText('Grape') + + await fireEvent.click(radioApple) + + expect(model.value).toBe('apple') + + await fireEvent.click(radioGrape) + + expect(model.value).toBe('grape') +}) + +it('should trigger event "change" when clicked', async () => { + const onChange = vi.fn() + const screen = render({ + components: { Radio }, + template : ` + + Apple + + `, + setup () { + return { + onChange, + } + } + }) + + const radioApple = screen.queryByText('Apple') + + await fireEvent.click(radioApple) + + expect(onChange).toBeCalledWith(true) +}) diff --git a/components/radio/Radio.vue b/components/radio/Radio.vue index 75907eea42..38507fb0c6 100644 --- a/components/radio/Radio.vue +++ b/components/radio/Radio.vue @@ -2,14 +2,27 @@

+ Apple + Grape + Orange +
+ + +**Selected :** + +
{{ selected }}
+ +```vue + +``` + +## Apperance + +Some case, you may need some [Checkbox](/checkbox/component) but work like a Radio. You can change the apperance via `apperance` props. + + +
+ Apple + Grape + Orange +
+
+ +**Selected :** + +
{{ selected }}
+ +```vue + +``` + +## API + +### Props + +| Props | Type | Default | Description | +|--------------|:---------:|:-------:|-------------------------------------------------------------------------| +| `checked` | `Boolean` | `false` | Checked condition. if it is `true`, Radio will be checked on first time | +| `value` | `Any` | `true` | Checked value | +| `disabled` | `Boolean` | `false` | Disable state | +| `readonly` | `Boolean` | `false` | Readonly state | +| `apperance` | `String` | `radio` | Radio apperance, valid value is: `radio`, `checkbox` | +| `modelValue` | `Any` | `-` | `v-model` value | + +### Slots + +| Name | Description | +|-----------|---------------------------| +| `default` | Content to place in radio | + +### Events + +| Name | Arguments | Description | +|----------|-----------|--------------------------| +| `change` | Boolean | Event when value changed | + +## See Also +- [Checkbox](/checkbox/component) +- [Toggle](/toggle/component) diff --git a/components/radio/use-radio.ts b/components/radio/use-radio.ts index 2d79d9039d..27d4ee858a 100644 --- a/components/radio/use-radio.ts +++ b/components/radio/use-radio.ts @@ -1,18 +1,20 @@ -import { computed, getCurrentInstance } from "vue-demi" +import { computed, getCurrentInstance, ref, watch } from "vue-demi" import type { CheckboxProps } from "../checkbox/use-checkbox" import { isEqual } from "../utils/value" export type RadioProps = Omit -export function useVModel

(props: P) { +export function useVModel (props: RadioProps) { const { emit } = getCurrentInstance() - const checked = props.value ?? true + const checked = props.value const model = computed({ get () { - return isEqual(props.modelValue, checked) + return isEqual(props.modelValue, checked) || props.checked }, set (value: boolean) { + emit('change', value) + if (value) emit('update:modelValue', checked) }, diff --git a/components/select/Select.vue b/components/select/Select.vue index 68b40a1ede..6e63f648df 100644 --- a/components/select/Select.vue +++ b/components/select/Select.vue @@ -111,13 +111,14 @@ export default defineComponent({ const keyword = ref('') const isOpen = ref(false) const isLoading = useLoading() - - const items = props.adapter.setup({ + const context = { props, keyword, isOpen, isLoading, - }) + } + + const items = props.adapter.setup(context) const model = computed({ get (): SelectItem { diff --git a/components/spinner/component.md b/components/spinner/component.md index 716a70782f..8f1fcc4e5b 100644 --- a/components/spinner/component.md +++ b/components/spinner/component.md @@ -83,3 +83,6 @@ + +## See Also +- [Overlay](/overlay/component) diff --git a/components/toggle/Toggle.spec.ts b/components/toggle/Toggle.spec.ts index c30751cc85..4b50ccc728 100644 --- a/components/toggle/Toggle.spec.ts +++ b/components/toggle/Toggle.spec.ts @@ -1,5 +1,6 @@ import { fireEvent, render } from "@testing-library/vue" +import { vi } from "vitest" import { ref } from "vue-demi" import Toggle from "./Toggle.vue" @@ -15,7 +16,7 @@ it('should render properly without any prop', () => { expect(toggle).toHaveClass('toggle', 'toggle--pill') }) -it('should have style "flat" if variant prop set to "flat"', () => { +it('should have style "flat" if `variant` prop set to "flat"', () => { const screen = render({ components: { Toggle }, template : `` @@ -88,6 +89,17 @@ it('should have readonly state if prop `readonly` is provided', () => { expect(toggle).toHaveClass('toggle', 'toggle--readonly') }) +it('should checked at start if prop `checked` is provided', () => { + const screen = render({ + components: { Toggle }, + template : ``, + }) + + const toggle = screen.queryByTestId('toggle') + + expect(toggle).toHaveClass('toggle--checked') +}) + it('should toggle the state if clicked', async () => { const screen = render({ components: { Toggle }, @@ -101,37 +113,48 @@ it('should toggle the state if clicked', async () => { await fireEvent.click(toggle) expect(toggle).toHaveClass('toggle--checked') + + await fireEvent.click(toggle) + + expect(toggle).not.toHaveClass('toggle--checked') }) -it('should modify state in v-model', async () => { - const model = ref(false) +it('should not toggle the state if disabled', async () => { const screen = render({ components: { Toggle }, - template : ` - - `, - setup () { - return { - model, - } - } + template : ``, }) const toggle = screen.queryByTestId('toggle') - expect(model.value).toBe(false) + expect(toggle).not.toHaveClass('toggle--checked') await fireEvent.click(toggle) - expect(model.value).toBe(true) + expect(toggle).not.toHaveClass('toggle--checked') +}) + +it('should not toggle the state if readonly', async () => { + const screen = render({ + components: { Toggle }, + template : ``, + }) + + const toggle = screen.queryByTestId('toggle') + + expect(toggle).not.toHaveClass('toggle--checked') + + await fireEvent.click(toggle) + + expect(toggle).not.toHaveClass('toggle--checked') }) -it('should use value in props `value` instead of true/false', async () => { +it('should modify state in v-model', async () => { const model = ref(false) const screen = render({ components: { Toggle }, template : ` - + `, setup () { return { @@ -146,11 +169,11 @@ it('should use value in props `value` instead of true/false', async () => { await fireEvent.click(toggle) - expect(model.value).toBe('On') + expect(model.value).toBe(true) }) it('should use value in props `value` and `unchecked-value` instead of true/false', async () => { - const model = ref(false) + const model = ref('') const screen = render({ components: { Toggle }, template : ` @@ -168,13 +191,13 @@ it('should use value in props `value` and `unchecked-value` instead of true/fals const toggle = screen.queryByTestId('toggle') - expect(model.value).toBe('Off') - await fireEvent.click(toggle) expect(model.value).toBe('On') await fireEvent.click(toggle) + + expect(model.value).toBe('Off') }) it('should append value if v-model is an array', async () => { @@ -205,3 +228,53 @@ it('should append value if v-model is an array', async () => { expect(model.value).toStrictEqual(['apple', 'grape', 'pineapple']) }) + +it('should remove value from array if toggle is clicked again', async () => { + const model = ref(['apple', 'grape', 'pineapple']) + const screen = render({ + components: { Toggle }, + template : ` + Apple + Grape + Pineapple + `, + setup () { + return { + model, + } + } + }) + + const toggleApple = screen.queryByText('Apple') + + expect(toggleApple).toHaveClass('toggle--checked') + + await fireEvent.click(toggleApple) + + expect(model.value).toStrictEqual(['grape', 'pineapple']) +}) + +it('should trigger event "change", when clicked', async () => { + const onChange = vi.fn() + const screen = render({ + components: { Toggle }, + template : ` + Apple + `, + setup () { + return { + onChange, + } + } + }) + + const toggle = screen.queryByTestId('toggle') + + await fireEvent.click(toggle) + + expect(onChange).toBeCalledWith(true) + + await fireEvent.click(toggle) + + expect(onChange).toBeCalledWith(false) +}) diff --git a/components/toggle/Toggle.vue b/components/toggle/Toggle.vue index 313b296278..c0f1d540af 100644 --- a/components/toggle/Toggle.vue +++ b/components/toggle/Toggle.vue @@ -33,6 +33,10 @@ type StyleVariant = 'pill' | 'flat' export default defineComponent({ props: { + variant: { + type : String as PropType, + default: 'pill' + }, modelValue: { default: false, }, @@ -46,10 +50,6 @@ export default defineComponent({ uncheckedValue: { default: false, }, - variant: { - type : String as PropType, - default: 'pill' - }, checkedLabel: { type : String, default: 'on', @@ -72,12 +72,12 @@ export default defineComponent({ }, }, models: { - prop: 'modelValue', + prop : 'modelValue', event: 'update:modelValue', }, emits: [ - 'change', 'update:modelValue', + 'change', ], setup (props, { emit }) { const model = useVModel(props) @@ -101,11 +101,9 @@ export default defineComponent({ }) function toggle () { - const newValue = !model.value - - model.value = newValue - - emit('change', newValue) + if (!props.readonly && !props.disabled) { + model.value = !model.value + } } return { diff --git a/components/toggle/component.md b/components/toggle/component.md index 570c965f6d..16b4eb7552 100644 --- a/components/toggle/component.md +++ b/components/toggle/component.md @@ -212,7 +212,7 @@ By default, value of toggle is always `Boolean`, but you can change it with `val ### Array v-model -Similar to [Checkbox][checkbox], if v-model **state** is an *array* it will append the value instead of replacing it. +Similar to [Checkbox](/checkbox/component), if v-model **state** is an *array* it will append the value instead of replacing it.

@@ -235,9 +235,37 @@ Similar to [Checkbox][checkbox], if v-model **state** is an *array* it will appe ``` -## See Also -- [Checkbox][checkbox] -- [Radio][radio] +## API + +### Props + +| Props | Type | Default | Description | +|------------------|:---------:|:-------:|--------------------------------------------------------------------------| +| `variant` | `String` | `pill` | Toggle style variant, valid value is `pill`, `flat` | +| `checked` | `Boolean` | `false` | Checked condition. if it is `true`, Toggle will be checked on first time | +| `value` | `Any` | `true` | Checked value | +| `uncheckedValue` | `Any` | `false` | Unchecked value | +| `checkedLabel` | `String` | `on` | Label when Toggle is checked | +| `uncheckedLabel` | `String` | `off` | Label when Toggle is unchecked | +| `noLabel` | `Boolean` | `false` | Hide label | +| `disabled` | `Boolean` | `false` | Disable state | +| `readonly` | `Boolean` | `false` | Readonly state | +| `modelValue` | `Any` | `-` | `v-model` value | + +### Slots -[checkbox]: /checkbox/component -[radio]: /radio/component +| Name | Description | +|-------------|-----------------------------| +| `default` | Content to place in toggle | +| `checked` | Content for checked label | +| `unchecked` | Content for unchecked label | + +### Events + +| Name | Arguments | Description | +|----------|-----------|--------------------------| +| `change` | Boolean | Event when value changed | + +## See Also +- [Checkbox](/checkbox/component) +- [Radio](/radio/component)