Skip to content

Commit

Permalink
TypeScript Rollout Tier 9 - Colorpicker & color (#374)
Browse files Browse the repository at this point in the history
* feat(lib): rewrite utils/color in TS

Rewrites `src/utils/color.js` in TypeScript.

Unrolls the loops to dynamically define the following Color components
as pairs of `get` and `set` methods:
- `red`
- `green`
- `blue`
- `alpha`
- `hue`
- `saturation`
- `lightness`

My attempt to augment the `Color` class with an ambient module did not
work when all the Buefy types were bundled in a single `.dts` file;
i.e., it won't work in users' environments.

Replaces `get [Symbol.toString]` with `get [Symbol.toStringTag]` to
suppress a type error, although, I am not 100% confident about this
solutation because I was not able to identify the intention of the
original code.

`color.ts` exports color model types: `Hsl`, `Hsla`, `Rgb`, and `Rgba`

* test(lib): rewrite spec for utils/color in TS

Rewrites `src/utils/color.spec.js→ts` in TypeScript. Imports the spec
building blocks from the `vitest` package.

* feat(lib): rewrite colorpicker in TS

Rewrites the colorpicker components in the `src/components/colorpicker`
in TypeScript.

In `Colorpicker.vue`:
- The `modelValue` prop, and its emitted value on "update:modelValue"
  have different types; `string | Rgb` vs `Color`. A user of
  `Colorpicker` may specify a string or an `Rgb` object to the prop
  unless `v-model` is necessary.
- Introduces a new type `IColorpicker` which represents a partial
  interface of `Colorpicker` to avoid circular references between
  `Colorpicker`, and `ColorFormatter` or `ColorParser`
- Introduces new types:
  - `ColorFormatter`: type of the `colorFormatter` prop
  - `ColorParser`: type of the `colorParser` prop
- No longer imports `Icon` and `Select`, because they are not used
- I found the following methods were not used but decided to leave them
  until we come back to deal with the `open-on-focus` and
  `close-on-click` props:
  - `handleOnFocus`
  - `toggle`: commented out because it contains an unfixable type error
  - `onInputClick`
  - `keyPress`
  - `togglePicker`: only called in `handleOnFocus` and `keyPress`

In `ColorpickerHSLRepresentationSquare.vue`, fixes a bug that the
`precision` custom template literal function tried to round `values`
instead of `values[i]`, while fixing the type error

In `ColorpickerHSLRepresentationSquare/Triangle.vue, the `value` prop,
and its emitted value on "input" have different types; `Hsl` vs `Color`

In `Colorpicker.vue`, and `ColorpickerAlphaSlider.vue`:
- Directly names Buefy components when they are registered so that they
  are type-checked. Note no type-checking is performed for components
  indirectly registered using the `name` field.

Replaces JSDoc-style comments with ordinary ones so that
`@microsoft/api-extractor` won't pick them up for documentation.

* test(lib): rewrite specs for colorpicker in TS

Rewrites the specs for the colorpicker components in the
`src/components/colorpicker` folder in TypeScript.

All the changes are straightforward. Imports the spec building blocks
from the `vitest` package.

In the `__snapshots__` folder, replaces the snapshots produced by Jest
with those by Vitest.

* chore(lib): bundle colorpicker in TS

`rollup.config.mjs` removes "colorpicker" from `JS_COMPONENTS`.

* feat(docs): rewrite colorpicker docs in TS

Rewrites the documentation for the colorpicker components in the
`src/pages/components/colorpicker` folder in TypeScript. All the changes
are straightforward.

Here is a TypeScript migration tip:
- Explicitly import and register components so that they are type
  checked. No type-checking is performed for globally registered
  components.
  • Loading branch information
kikuomax authored Jan 12, 2025
1 parent deaf539 commit 8308232
Show file tree
Hide file tree
Showing 28 changed files with 503 additions and 347 deletions.
1 change: 0 additions & 1 deletion packages/buefy-next/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const components = fs

const JS_COMPONENTS = [
'clockpicker',
'colorpicker',
'datepicker',
'datetimepicker',
'numberinput',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import BColorpicker from '@components/colorpicker/Colorpicker'
import BColorpicker from '@components/colorpicker/Colorpicker.vue'

describe('BColorpicker', () => {
it('render correctly', () => {
Expand Down
134 changes: 82 additions & 52 deletions packages/buefy-next/src/components/colorpicker/Colorpicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,49 +98,68 @@
</div>
</template>

<script>
<script lang="ts">
import { defineComponent } from 'vue'
import type { DefineComponent, PropType } from 'vue'
import FormElementMixin from '../../utils/FormElementMixin'
import { isMobile } from '../../utils/helpers'
import config from '../../utils/config'
import Color from '../../utils/color'
import type { Rgb } from '../../utils/color'
import BButton from '../button/Button.vue'
import BDropdown from '../dropdown/Dropdown.vue'
import BDropdownItem from '../dropdown/DropdownItem.vue'
import BInput from '../input/Input.vue'
import BField from '../field/Field.vue'
import BColorpickerHSLRepresentationTriangle from './ColorpickerHSLRepresentationTriangle.vue'
import BColorpickerHSLRepresentationSquare from './ColorpickerHSLRepresentationSquare.vue'
import BColorpickerAlphaSlider from './ColorpickerAlphaSlider.vue'
import Button from '../button/Button.vue'
import Dropdown from '../dropdown/Dropdown.vue'
import DropdownItem from '../dropdown/DropdownItem.vue'
import Input from '../input/Input.vue'
import Field from '../field/Field.vue'
import Select from '../select/Select.vue'
import Icon from '../icon/Icon.vue'
type BDropdownInstance = InstanceType<typeof BDropdown>
import ColorpickerHSLRepresentationTriangle from './ColorpickerHSLRepresentationTriangle.vue'
import ColorpickerHSLRepresentationSquare from './ColorpickerHSLRepresentationSquare.vue'
import ColorpickerAlphaSlider from './ColorpickerAlphaSlider.vue'
// Interface of the `Colorpicker` component.
// Gives a separate definition to avoid circular dependencies.
// TODO: we have to learn about which properties are to be exposed to the
// implementors of custom `ColorFormatter`, and `ColorParser`.
/* eslint-disable @typescript-eslint/ban-types */
export type IColorpicker = Omit<DefineComponent<
{}, // P(rops)
{}, // B (raw bindings)
{}, // D(ata)
{}, // C(omputed)
{} // M(ethods)
>, '$emit'> // default E(mits) type parameter would causes a `$emit` discrepancy
/* eslint-enable @typescript-eslint/ban-types */
const defaultColorFormatter = (color, vm) => {
export type ColorFormatter = (color: Color, vm?: IColorpicker) => string
export type ColorParser = (color: string | Rgb, vm?: IColorpicker) => Color
const defaultColorFormatter: ColorFormatter = (color) => {
if (color.alpha < 1) {
return color.toString('hexa')
} else {
return color.toString('hex')
}
}
const defaultColorParser = (color, vm) => {
const defaultColorParser: ColorParser = (color) => {
return Color.parse(color)
}
export default {
export default defineComponent({
name: 'BColorpicker',
components: {
[ColorpickerHSLRepresentationTriangle.name]: ColorpickerHSLRepresentationTriangle,
[ColorpickerHSLRepresentationSquare.name]: ColorpickerHSLRepresentationSquare,
[ColorpickerAlphaSlider.name]: ColorpickerAlphaSlider,
[Input.name]: Input,
[Field.name]: Field,
[Select.name]: Select,
[Icon.name]: Icon,
[Button.name]: Button,
[Dropdown.name]: Dropdown,
[DropdownItem.name]: DropdownItem
BColorpickerHSLRepresentationTriangle,
BColorpickerHSLRepresentationSquare,
BColorpickerAlphaSlider,
BInput,
BField,
BButton,
BDropdown,
BDropdownItem
},
mixins: [FormElementMixin],
inheritAttrs: false,
Expand All @@ -151,8 +170,8 @@ export default {
},
props: {
modelValue: {
type: [String, Object],
validator(value) {
type: [String, Object] as PropType<string | Rgb>,
validator(value: string | Rgb) {
return typeof value === 'string' ||
(
typeof value === 'object' &&
Expand All @@ -169,7 +188,7 @@ export default {
representation: {
type: String,
default: 'triangle',
value(value) {
value(value: string) {
return ['triangle', 'square'].some((r) => r === value)
}
},
Expand All @@ -180,8 +199,8 @@ export default {
default: false
},
colorFormatter: {
type: Function,
default: (color, vm) => {
type: Function as PropType<ColorFormatter>,
default: (color: Color, vm?: IColorpicker) => {
if (typeof config.defaultColorFormatter === 'function') {
return config.defaultColorFormatter(color)
} else {
Expand All @@ -190,8 +209,8 @@ export default {
}
},
colorParser: {
type: Function,
default: (color, vm) => {
type: Function as PropType<ColorParser>,
default: (color: string, vm?: IColorpicker) => {
if (typeof config.defaultColorParser === 'function') {
return config.defaultColorParser(color)
} else {
Expand All @@ -217,17 +236,24 @@ export default {
type: Boolean,
default: () => config.defaultTrapFocus
},
openOnFocus: Boolean,
closeOnClick: Boolean,
appendToBody: Boolean
},
emits: ['active-change', 'update:modelValue'],
emits: {
/* eslint-disable @typescript-eslint/no-unused-vars */
'active-change': (_active: boolean) => true,
'update:modelValue': (_value: Color) => true
/* eslint-enable @typescript-eslint/no-unused-vars */
},
data() {
return {
color: this.parseColor(this.modelValue)
}
},
computed: {
computedValue: {
set(value) {
set(value: string | Rgb) {
this.color = this.parseColor(value)
},
get() {
Expand Down Expand Up @@ -286,19 +312,19 @@ export default {
}
},
methods: {
parseColor(color) {
parseColor(color: string | Rgb | Color) {
try {
return this.colorParser(color)
} catch (e) {
return new Color()
}
},
updateColor(value) {
updateColor(value: Color) {
value.alpha = this.computedValue.alpha
this.computedValue = value
this.$emit('update:modelValue', value)
},
updateAlpha(alpha) {
updateAlpha(alpha: number) {
this.computedValue.alpha = alpha
this.$emit('update:modelValue', this.computedValue)
},
Expand All @@ -308,30 +334,30 @@ export default {
/*
* Format color into string
*/
formatValue(value) {
formatValue(value: Color) { // FIXME: unused?
return value ? this.colorFormatter(value, this) : null
},
/*
* Toggle datepicker
*/
togglePicker(active) {
togglePicker(active: boolean) {
if (this.$refs.dropdown) {
const isActive = typeof active === 'boolean'
? active
: !this.$refs.dropdown.isActive
: !(this.$refs.dropdown as BDropdownInstance).isActive
if (isActive) {
this.$refs.dropdown.isActive = isActive
(this.$refs.dropdown as BDropdownInstance).isActive = isActive
} else if (this.closeOnClick) {
this.$refs.dropdown.isActive = isActive
(this.$refs.dropdown as BDropdownInstance).isActive = isActive
}
}
},
/*
* Call default onFocus method and show datepicker
*/
handleOnFocus(event) {
handleOnFocus(event: Event) {
this.onFocus(event)
if (this.openOnFocus) {
this.togglePicker(true)
Expand All @@ -341,38 +367,42 @@ export default {
/*
* Toggle dropdown
*/
// I decided to comment out the following unused method until we come
// back to deal with the `open-on-focus` and `close-on-click` props
/*
toggle() {
if (this.mobileNative && this.isMobile) {
const input = this.$refs.input.$refs.input
input.focus()
input.click()
return
}
this.$refs.dropdown.toggle()
},
(this.$refs.dropdown as BDropdownInstance).toggle()
}, */
/*
* Avoid dropdown toggle when is already visible
*/
onInputClick(event) {
if (this.$refs.dropdown.isActive) {
onInputClick(event: Event) {
if ((this.$refs.dropdown as BDropdownInstance).isActive) {
event.stopPropagation()
}
},
/**
/*
* Keypress event that is bound to the document.
*/
keyPress({ key }) {
if (this.$refs.dropdown && this.$refs.dropdown.isActive && (key === 'Escape' || key === 'Esc')) {
keyPress({ key }: KeyboardEvent) {
const dropdown = this.$refs.dropdown as BDropdownInstance
if (dropdown && dropdown.isActive && (key === 'Escape' || key === 'Esc')) {
this.togglePicker(false)
}
},
/**
/*
* Emit 'blur' event on dropdown is not active (closed)
*/
onActiveChange(value) {
onActiveChange(value: boolean) {
if (!value) {
this.onBlur()
}
Expand All @@ -382,5 +412,5 @@ export default {
this.$emit('active-change', value)
}
}
}
})
</script>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Color from '@utils/color'
import BColorpickerAlphaSlider from '@components/colorpicker/ColorpickerAlphaSlider'
import BColorpickerAlphaSlider from '@components/colorpicker/ColorpickerAlphaSlider.vue'

describe('BColorpickerAlphaSlider', () => {
it('render correctly', () => {
Expand Down
Loading

0 comments on commit 8308232

Please sign in to comment.