Skip to content

Commit

Permalink
feat(Radio/Checkbox/Toggle)!: handle color prop for form elements (#…
Browse files Browse the repository at this point in the history
…323)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
  • Loading branch information
Haythamasalama and benjamincanac committed Jun 21, 2023
1 parent 97a1c86 commit ffb312d
Show file tree
Hide file tree
Showing 16 changed files with 207 additions and 45 deletions.
4 changes: 3 additions & 1 deletion docs/components/content/ComponentCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
v-if="prop.type === 'boolean'"
v-model="componentProps[prop.name]"
:name="`prop-${prop.name}`"
variant="none"
tabindex="-1"
:ui="{ wrapper: 'relative flex items-start justify-center' }"
/>
<USelectMenu
Expand All @@ -18,6 +18,7 @@
variant="none"
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md', wrapper: 'relative inline-flex' }"
class="!py-0"
tabindex="-1"
:popper="{ strategy: 'fixed', placement: 'bottom-start' }"
/>
<UInput
Expand All @@ -28,6 +29,7 @@
variant="none"
autocomplete="off"
class="!py-0"
tabindex="-1"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion docs/components/content/examples/CheckboxExample.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup>
const selected = ref(false)
const selected = ref(true)
</script>

<template>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/1.getting-started/3.theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it
We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`.
::

Components having a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style), [Input](/elements/input#style) (inherited in [Select](/forms/select) and [SelectMenu](/forms/select-menu)), [Range](/forms/range) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.
Components having a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style), [Input](/elements/input#style) (inherited in [Select](/forms/select) and [SelectMenu](/forms/select-menu)), [Radio](/forms/radio), [Checkbox](/forms/checkbox), [Toggle](/forms/toggle), [Range](/forms/range) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.

Variant classes of those components are defined with a syntax like `bg-{color}-500 dark:bg-{color}-400` so they can be used with any color. However, this means that Tailwind will not find those classes and therefore will not generate the corresponding CSS.

Expand Down
20 changes: 17 additions & 3 deletions docs/content/3.forms/5.checkbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Use a `v-model` to make the Checkbox reactive.
#code
```vue
<script setup>
const selected = ref(false)
const selected = ref(true)
</script>
<template>
Expand All @@ -36,14 +36,28 @@ props:
---
::

### Style

Use the `color` prop to change the style of the Checkbox.

::component-card
---
baseProps:
name: 'checkbox2'
label: 'Label'
props:
color: 'primary'
---
::

### Required

Use the `required` prop to display a red star next to the label.

::component-card
---
baseProps:
name: 'checkbox2'
name: 'checkbox3'
props:
label: 'Label'
required: true
Expand All @@ -57,7 +71,7 @@ Use the `help` prop to display some text under the Checkbox.
::component-card
---
baseProps:
name: 'checkbox3'
name: 'checkbox4'
props:
label: 'Label'
help: 'Please check this box'
Expand Down
20 changes: 17 additions & 3 deletions docs/content/3.forms/6.radio.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,28 @@ props:
---
::

### Style

Use the `color` prop to change the style of the Radio.

::component-card
---
baseProps:
name: 'radio2'
label: 'Label'
props:
color: 'primary'
---
::

### Required

Use the `required` prop to display a red star next to the label.

::component-card
---
baseProps:
name: 'radio2'
name: 'radio3'
props:
label: 'Label'
required: true
Expand All @@ -71,7 +85,7 @@ Use the `help` prop to display some text under the Radio.
::component-card
---
baseProps:
name: 'radio3'
name: 'radio4'
props:
label: 'Label'
help: 'Please choose one'
Expand All @@ -85,7 +99,7 @@ Use the `disabled` prop to disable the Radio.
::component-card
---
baseProps:
name: 'radio4'
name: 'radio5'
value: true
props:
disabled: true
Expand Down
11 changes: 11 additions & 0 deletions docs/content/3.forms/7.toggle.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ const selected = ref(false)
```
::

### Style

Use the `color` prop to change the style of the Toggle.

::component-card
---
props:
color: 'primary'
---
::

### Icon

Use any icon from [Iconify](https://icones.js.org) by setting the `on-icon` and `off-icon` props by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.toggle.default.onIcon` and `ui.toggle.default.offIcon`.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/6.overlays/6.notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ props:
---
::

### Color
### Style

Use the `color` prop to change the progress and icon color of the Notification.

Expand Down
43 changes: 42 additions & 1 deletion src/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,47 @@ const safelistByComponent = {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus']
}],
radio: (colorsAsRegex) => [{
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
checkbox: (colorsAsRegex) => [{
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
toggle: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
range: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
Expand Down Expand Up @@ -144,7 +185,7 @@ const colorsAsRegex = (colors: string[]): string => colors.join('|')
export const excludeColors = (colors: object) => Object.keys(omit(colors, colorsToExclude)).map(color => kebabCase(color)) as string[]

export const generateSafelist = (colors: string[]) => {
const safelist = ['avatar', 'badge', 'button', 'input', 'range', 'notification'].flatMap(component => safelistByComponent[component](colorsAsRegex(colors)))
const safelist = Object.keys(safelistByComponent).flatMap(component => safelistByComponent[component](colorsAsRegex(colors)))

return [
...safelist,
Expand Down
33 changes: 25 additions & 8 deletions src/runtime/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,24 +473,40 @@ const selectMenu = {

const radio = {
wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
base: 'h-4 w-4 dark:checked:bg-current dark:checked:border-transparent disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
color: 'text-{color}-500 dark:text-{color}-400',
background: 'bg-white dark:bg-gray-900',
border: 'border border-gray-300 dark:border-gray-700',
ring: 'focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-500 dark:text-red-400',
help: 'text-gray-500 dark:text-gray-400'
help: 'text-gray-500 dark:text-gray-400',
default: {
color: 'primary'
}
}

const checkbox = {
wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent dark:indeterminate:bg-current dark:indeterminate:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
base: 'h-4 w-4 dark:checked:bg-current dark:checked:border-transparent dark:indeterminate:bg-current dark:indeterminate:border-transparent disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
rounded: 'rounded',
color: 'text-{color}-500 dark:text-{color}-400',
background: 'bg-white dark:bg-gray-900',
border: 'border border-gray-300 dark:border-gray-700',
ring: 'focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-500 dark:text-red-400',
help: 'text-gray-500 dark:text-gray-400'
help: 'text-gray-500 dark:text-gray-400',
default: {
color: 'primary'
}
}

const toggle = {
base: 'relative inline-flex flex-shrink-0 h-5 w-9 border-2 border-transparent rounded-full cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
active: 'bg-primary-500 dark:bg-primary-400',
base: 'relative inline-flex h-5 w-9 flex-shrink-0 border-2 border-transparent disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none',
rounded: 'rounded-full',
ring: 'focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
active: 'bg-{color}-500 dark:bg-{color}-400',
inactive: 'bg-gray-200 dark:bg-gray-700',
container: {
base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white dark:bg-gray-900 shadow transform ring-0 transition ease-in-out duration-200',
Expand All @@ -501,12 +517,13 @@ const toggle = {
base: 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
active: 'opacity-100 ease-in duration-200',
inactive: 'opacity-0 ease-out duration-100',
on: 'h-3 w-3 text-primary-500 dark:text-primary-400',
on: 'h-3 w-3 text-{color}-500 dark:text-{color}-400',
off: 'h-3 w-3 text-gray-400 dark:text-gray-500'
},
default: {
onIcon: null,
offIcon: null
offIcon: null,
color: 'primary'
}
}

Expand Down
32 changes: 25 additions & 7 deletions src/runtime/components/forms/Checkbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="flex items-center h-5">
<input
:id="name"
v-model="isChecked"
v-model="toggle"
:name="name"
:required="required"
:value="value"
Expand All @@ -12,10 +12,8 @@
:indeterminate="indeterminate"
type="checkbox"
class="form-checkbox"
:class="[ui.base, ui.rounded]"
:class="inputClass"
v-bind="$attrs"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
</div>
<div v-if="label || $slots.label" class="ml-3 text-sm">
Expand All @@ -34,6 +32,7 @@
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
Expand Down Expand Up @@ -80,19 +79,26 @@ export default defineComponent({
type: Boolean,
default: false
},
color: {
type: String,
default: () => appConfig.ui.checkbox.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.checkbox>>,
default: () => appConfig.ui.checkbox
}
},
emits: ['update:modelValue', 'focus', 'blur'],
emits: ['update:modelValue'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defu({}, props.ui, appConfig.ui.checkbox))
const isChecked = computed({
const toggle = computed({
get () {
return props.modelValue
},
Expand All @@ -101,10 +107,22 @@ export default defineComponent({
}
})
const inputClass = computed(() => {
return classNames(
ui.value.base,
ui.value.rounded,
ui.value.background,
ui.value.border,
ui.value.ring.replaceAll('{color}', props.color),
ui.value.color.replaceAll('{color}', props.color)
)
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
isChecked
toggle,
inputClass
}
}
})
Expand Down
4 changes: 1 addition & 3 deletions src/runtime/components/forms/Input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
:class="inputClass"
v-bind="$attrs"
@input="onInput"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
<slot />

Expand Down Expand Up @@ -140,7 +138,7 @@ export default defineComponent({
default: () => appConfig.ui.input
}
},
emits: ['update:modelValue', 'focus', 'blur'],
emits: ['update:modelValue'],
setup (props, { emit, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
Expand Down
Loading

0 comments on commit ffb312d

Please sign in to comment.