Skip to content

Commit

Permalink
refactor: Added @modulify/validator for inner props validation
Browse files Browse the repository at this point in the history
  • Loading branch information
cmath10 committed Dec 30, 2024
1 parent be59b88 commit 7ecaa84
Show file tree
Hide file tree
Showing 21 changed files with 182 additions and 81 deletions.
14 changes: 7 additions & 7 deletions m3-foundation/lib/popper/Listener.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type {
Trigger,
TriggerHandler,
TriggerOptions,
} from '@modulify/m3-foundation/types/components/popper'
TriggerSchema,
} from '../../types/components/popper'

import isEqual from 'lodash.isequal'

const isArray = Array.isArray
const normalize = (triggers: Trigger[] | TriggerOptions): Required<TriggerOptions> => {
const normalize = (triggers: Trigger[] | TriggerSchema): Required<TriggerSchema> => {
const show = isArray(triggers) ? triggers : triggers.show ?? []
const hide = isArray(triggers) ? triggers : triggers.hide ?? []

Expand All @@ -19,7 +19,7 @@ const normalize = (triggers: Trigger[] | TriggerOptions): Required<TriggerOption

export default class Listener {
private _target: EventTarget | null = null
private _triggers: Required<TriggerOptions> = {
private _triggers: Required<TriggerSchema> = {
show: [],
hide: [],
}
Expand Down Expand Up @@ -53,11 +53,11 @@ export default class Listener {
}
}

get triggers (): Required<TriggerOptions> {
get triggers (): Required<TriggerSchema> {
return this._triggers
}

set triggers (triggers: Trigger[] | TriggerOptions) {
set triggers (triggers: Trigger[] | TriggerSchema) {
if (!isEqual(triggers, this._triggers)) {
this.stop()
this._triggers = normalize(triggers)
Expand All @@ -67,7 +67,7 @@ export default class Listener {
}
}

start (target: EventTarget, triggers: Trigger[] | TriggerOptions) {
start (target: EventTarget, triggers: Trigger[] | TriggerSchema) {
this._target = target
this._triggers = normalize(triggers)
this._subscribe()
Expand Down
2 changes: 1 addition & 1 deletion m3-foundation/lib/popper/closer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {
CloserEvent,
CloserTarget,
} from '@modulify/m3-foundation/types/components/popper'
} from '../../types/components/popper'

export const onClick = (event: CloserEvent) => {
const el = event.currentTarget as CloserTarget
Expand Down
2 changes: 1 addition & 1 deletion m3-foundation/lib/popper/floating.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FloatingOptions } from '@modulify/m3-foundation/types/components/popper'
import type { FloatingOptions } from '../../types/components/popper'
import type { Middleware } from '@floating-ui/dom'

import {
Expand Down
49 changes: 38 additions & 11 deletions m3-foundation/lib/popper/predicates.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
import type {
OverflowBehavior,
Placement,
Trigger,
} from '../../types/components/popper'

import {
arrayOf,
isArrayOf,
isElement,
isExactly,
isNumeric,
isShape,
oneOf,
} from '../predicates'

export const isBoundary = oneOf(isExactly('clippingAncestors'), isElement, arrayOf(isElement))
import {
OneOf,
} from '@modulify/validator/assertions'

import {
isExact,
Or,
} from '@modulify/validator/predicates'

export const isBoundary = Or(isExact('clippingAncestors' as const), isElement, isArrayOf(isElement))

export const isDelay = Or(isNumeric, isShape({
show: isNumeric,
hide: isNumeric,
}))

export const isOverflowBehavior = isArrayOf(OneOf<OverflowBehavior>(['flip', 'shift', 'hide']))

export const isOverflowBehavior = arrayOf((value: unknown) => {
return (['flip', 'shift', 'hide'] as OverflowBehavior[]).includes(value as OverflowBehavior)
})
export const isPlacement = OneOf<Placement>([
'left',
'left-start',
'left-end',
'top',
'top-start',
'top-end',
'right',
'right-start',
'right-end',
'bottom',
'bottom-start',
'bottom-end',
])

export const isTrigger = (value: unknown) => (['hover', 'focus', 'click', 'touch'] as Trigger[]).includes(value as Trigger)
export const isTriggerOptions = oneOf(arrayOf(isTrigger), isShape({
show: arrayOf(isTrigger),
hide: arrayOf(isTrigger),
export const isTrigger = OneOf<Trigger>(['hover', 'focus', 'click', 'touch'])
export const isTriggerOptions = Or(isArrayOf(isTrigger), isShape({
show: isArrayOf(isTrigger),
hide: isArrayOf(isTrigger),
}))
2 changes: 1 addition & 1 deletion m3-foundation/lib/popper/scheduling.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Delay } from '@modulify/m3-foundation/types/components/popper'
import type { Delay } from '../../types/components/popper'

export const normalizeDelay = (delay: number | string | Delay): {
show: number;
Expand Down
60 changes: 45 additions & 15 deletions m3-foundation/lib/predicates.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,60 @@
export type Predicate = (value: unknown) => boolean
import type { Predicate } from '@modulify/validator/types'

export const arrayOf = (predicate: Predicate) => {
import {
isArray,
isString,
} from '@modulify/validator/predicates'

export type { Predicate }

export {
isArray,
isNull,
isString,
isUndefined,
Or,
} from '@modulify/validator/predicates'

export const isArrayOf = <T>(predicate: Predicate<T>): Predicate<T[]> => {
return (value: unknown) => isArray(value) && value.every(predicate)
}

export const isArray = Array.isArray
export const isId = (x: unknown): x is string => isString(x) && x.length > 0 && /^[A-Za-z]/.test(x)

export const isElement = (value: unknown) => value instanceof Element

export const isExactly = <T>(subject: T) => (value: unknown) => value === subject

export const isHTMLElement = (value: unknown) => value instanceof HTMLElement

export const isNull = (value: unknown): value is null => value === null
export const isNumeric = (value: unknown) => !isNaN(Number(value))
export const isNumeric: Predicate<number | string> = (value: unknown): value is number | string => !isNaN(Number(value))

export type Shape<T extends object> = {
[K in keyof T]: [Predicate<T[K]>, boolean] | Predicate<T[K]>
}

export type IsRequired<T extends Shape<object>, K extends keyof T> =
T[K] extends [unknown, infer R extends boolean]
? R
: false

type ShapeRequired<S extends Shape<object>> = {
[K in keyof S as IsRequired<S, K> extends true ? K : never]: S[K]
}

export const isShape = (shape: Record<string, [Predicate, boolean] | Predicate>) => {
type ShapeOptional<S extends Shape<object>> = {
[K in keyof S as IsRequired<S, K> extends false ? K : never]: S[K]
}

type ExtractType<S> = S extends Shape<infer T> ? T : never

export type ExtractRequired<S extends Shape<object>> = ExtractType<ShapeRequired<S>>
export type ExtractOptional<S extends Shape<object>> = Partial<ExtractType<ShapeOptional<S>>>

type TypeOf<S extends Shape<object>> = ExtractRequired<S> & ExtractOptional<S>

export const isShape = <S extends Shape<object>>(shape: S) => {
const properties = Object.keys(shape)

return (value: unknown) => typeof value === 'object' && value !== null && properties.every(p => {
return (value: unknown): value is TypeOf<S> => typeof value === 'object' && value !== null && properties.every(p => {
const config = shape[p]
const [predicate, required] = isArray(config) ? config : [config, false]
if (!(p in value)) {
Expand All @@ -28,9 +64,3 @@ export const isShape = (shape: Record<string, [Predicate, boolean] | Predicate>)
return predicate(value[p])
})
}

export const isString = (value: unknown): value is string => typeof value === 'string'

export const oneOf = (...predicates: Predicate[]): Predicate => {
return (value: unknown) => predicates.some(p => p(value))
}
1 change: 1 addition & 0 deletions m3-foundation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@modulify/validator": "^0.1.0",
"@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.17.1",
"@typescript-eslint/eslint-plugin": "^8.11.0",
Expand Down
12 changes: 7 additions & 5 deletions m3-foundation/types/components/popper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import type {
Strategy,
} from '@floating-ui/dom'

export type OverflowBehavior = 'flip' | 'shift' | 'hide'

export type Delay = {
show?: number | string;
hide?: number | string;
}

export type OverflowBehavior = 'flip' | 'shift' | 'hide'

export type { Placement }

export type Trigger = 'hover' | 'focus' | 'click' | 'touch'
export type TriggerOptions = {
export type TriggerSchema = {
show?: Trigger[],
hide?: Trigger[],
}
Expand All @@ -31,8 +33,8 @@ export type FloatingOptions = {
}

export type ListeningOptions = {
targetTriggers?: Trigger[] | TriggerOptions;
popperTriggers?: Trigger[] | TriggerOptions;
targetTriggers?: Trigger[] | TriggerSchema;
popperTriggers?: Trigger[] | TriggerSchema;
hideOnMissClick?: boolean;
}

Expand Down
6 changes: 3 additions & 3 deletions m3-react/src/components/badge/M3Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react'
import type { FC, HTMLAttributes } from 'react'

import { isEmptyNode } from '@/utils/content'
import { toClassName } from '@/utils/styling'

export interface M3BadgeProps extends React.HTMLAttributes<HTMLElement> {
export interface M3BadgeProps extends HTMLAttributes<HTMLElement> {
label?: string;
}

const M3Badge: React.FC<M3BadgeProps> = ({
const M3Badge: FC<M3BadgeProps> = ({
label = '',
className = '',
children = null,
Expand Down
6 changes: 3 additions & 3 deletions m3-react/src/components/popper/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
Delay,
OverflowBehavior,
Trigger,
TriggerOptions,
TriggerSchema,
} from '@modulify/m3-foundation/types/components/popper'

import type {
Expand All @@ -17,8 +17,8 @@ type HideReason = 'generic' | 'by-closer' | 'by-miss-click'

export interface M3PopperProps extends HTMLAttributes<HTMLElement> {
target: Element | null;
targetTriggers?: Trigger[] | TriggerOptions;
popperTriggers?: Trigger[] | TriggerOptions;
targetTriggers?: Trigger[] | TriggerSchema;
popperTriggers?: Trigger[] | TriggerSchema;
shown?: boolean;
hideOnMissClick?: boolean;
placement?: Placement;
Expand Down
11 changes: 8 additions & 3 deletions m3-vue/src/components/checkbox/M3Checkbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,19 @@ import {
ref,
} from 'vue'
import makeId from '@/utils/id'
import {
isArray,
isId,
isUndefined,
Or,
} from '@modulify/m3-foundation/lib/predicates'
const isArray = Array.isArray
import makeId from '@/utils/id'
const props = defineProps({
id: {
type: null as unknown as PropType<string | undefined>,
validator: (id: unknown) => id === undefined || typeof id === 'string' && id.length > 0 && /^[A-Za-z]/.test(id),
validator: Or(isId, isUndefined),
default: undefined,
},
Expand Down
8 changes: 6 additions & 2 deletions m3-vue/src/components/icon/M3Icon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@
</template>

<script lang="ts" setup>
import type { PropType, Ref } from 'vue'
import type { Appearance } from '@modulify/m3-foundation/types/components/icon'
import type {
PropType,
Ref,
} from 'vue'
import { M3IconAppearance } from '@/components/icon'
import {
computed,
inject,
ref,
} from 'vue'
import { M3IconAppearance } from '@/components/icon'
const props = defineProps({
name: {
type: String,
Expand Down
11 changes: 7 additions & 4 deletions m3-vue/src/components/menu/M3Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ import {
isNull,
isNumeric,
isString,
oneOf,
Or,
} from '@modulify/m3-foundation/lib/predicates'
import {
isBoundary,
isDelay,
isPlacement,
} from '@modulify/m3-foundation/lib/popper/predicates'
defineProps({
Expand All @@ -69,6 +71,7 @@ defineProps({
placement: {
type: String as PropType<Placement>,
validator: isPlacement,
default: 'bottom',
},
Expand Down Expand Up @@ -97,7 +100,7 @@ defineProps({
container: {
type: null as unknown as PropType<string | HTMLElement>,
validator: oneOf(isString, isHTMLElement),
validator: Or(isString, isHTMLElement),
default: 'body',
},
Expand All @@ -108,13 +111,13 @@ defineProps({
delay: {
type: [Number, String, Object] as PropType<number | string | Delay>,
validator: (value: number | string | Delay) => typeof value === 'object' || !isNaN(Number(value)),
validator: isDelay,
default: () => ({ hide: 200 }),
},
detachTimeout: {
type: null as unknown as PropType<number | string | null>,
validator: oneOf(isNull, isNumeric),
validator: Or(isNull, isNumeric),
default: 5000,
},
Expand Down
Loading

0 comments on commit 7ecaa84

Please sign in to comment.