Skip to content

Commit

Permalink
refactor(*): use-field hook
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Aug 12, 2024
1 parent 96e4cdd commit c8179a7
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 33 deletions.
Binary file modified bun.lockb
Binary file not shown.
5 changes: 4 additions & 1 deletion packages/react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ description: All notable changes will be documented in this file.

### Fixed

- **Field**: Fixed SSR warning related to `useLayoutEffect` usage.
- **Field**:
- Fixed SSR warning related to `useLayoutEffect` usage.
- Fixed issue where id of field parts could not be customized, breaking Zag.js composition.
- Added `data-*` attributes to control part to allow for better styling.

## [3.6.2] - 2024-07-28

Expand Down
39 changes: 28 additions & 11 deletions packages/react/src/components/field/use-field.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import { getWindow } from '@zag-js/dom-query'
import { ariaAttr, dataAttr, getWindow } from '@zag-js/dom-query'
import { useId, useMemo, useRef } from 'react'
import { useSafeLayoutEffect } from '../../utils/use-safe-layout-effect'
import type { HTMLProps } from '../factory'
import { useFieldsetContext } from '../fieldset/use-fieldset-context'
import { parts } from './field.anatomy'

export interface ElementIds {
root?: string
control?: string
label?: string
errorText?: string
helperText?: string
}

export interface UseFieldProps {
/**
* The id of the field.
*/
id?: string
/**
* The ids of the field parts.
*/
ids?: ElementIds
/**
* Indicates whether the field is required.
*/
Expand All @@ -30,6 +45,7 @@ export type UseFieldReturn = ReturnType<typeof useField>
export const useField = (props: UseFieldProps) => {
const fieldset = useFieldsetContext()
const {
ids,
disabled = Boolean(fieldset?.disabled),
invalid = false,
readOnly = false,
Expand All @@ -41,9 +57,11 @@ export const useField = (props: UseFieldProps) => {

const id = props.id ?? useId()
const rootRef = useRef<HTMLDivElement>(null)
const errorTextId = `field::${id}::error-text`
const helperTextId = `field::${id}::helper-text`
const labelId = `field::${id}::label`

const rootId = ids?.control ?? `field::${id}`
const errorTextId = ids?.errorText ?? `field::${id}::error-text`
const helperTextId = ids?.helperText ?? `field::${id}::helper-text`
const labelId = ids?.label ?? `field::${id}::label`

useSafeLayoutEffect(() => {
const rootNode = rootRef.current
Expand Down Expand Up @@ -75,13 +93,14 @@ export const useField = (props: UseFieldProps) => {
() => () =>
({
...parts.root.attrs,
id: rootId,
ref: rootRef,
role: 'group',
'data-disabled': dataAttr(disabled),
'data-invalid': dataAttr(invalid),
'data-readonly': dataAttr(readOnly),
}) as HTMLProps<'div'>,
[disabled, invalid, readOnly],
[disabled, invalid, readOnly, rootId],
)

const getLabelProps = useMemo(
Expand All @@ -102,8 +121,9 @@ export const useField = (props: UseFieldProps) => {
({
'aria-describedby': labelIds,
'aria-invalid': ariaAttr(invalid),
'aria-required': ariaAttr(required),
'aria-readonly': ariaAttr(readOnly),
'data-invalid': dataAttr(invalid),
'data-required': dataAttr(required),
'data-readonly': dataAttr(readOnly),
id,
required,
disabled,
Expand Down Expand Up @@ -161,6 +181,7 @@ export const useField = (props: UseFieldProps) => {
return {
ariaDescribedby: labelIds,
ids: {
root: rootId,
control: id,
label: labelId,
errorText: errorTextId,
Expand All @@ -182,7 +203,3 @@ export const useField = (props: UseFieldProps) => {
getErrorTextProps,
}
}

type Booleanish = boolean | 'true' | 'false'
const dataAttr = (condition: boolean | undefined) => (condition ? 'true' : undefined) as Booleanish
const ariaAttr = (condition: boolean | undefined) => (condition ? true : undefined)
6 changes: 6 additions & 0 deletions packages/solid/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ description: All notable changes will be documented in this file.

- **Progress**: Update `Progress.ValueText` to render percentage as string.

### Fixed

- **Field**:
- Fixed issue where id of field parts could not be customized, breaking Zag.js composition.
- Added `data-*` attributes to control part to allow for better styling.

## [3.6.2] - 2024-07-28

### Changed
Expand Down
37 changes: 27 additions & 10 deletions packages/solid/src/components/field/use-field.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { getWindow } from '@zag-js/dom-query'
import { ariaAttr, dataAttr, getWindow } from '@zag-js/dom-query'
import { createEffect, createMemo, createSignal, createUniqueId, onCleanup } from 'solid-js'
import { useFieldsetContext } from '../fieldset'
import type { UseFieldsetReturn } from '../fieldset/use-fieldset'
import { parts } from './field.anatomy'

export interface ElementIds {
root?: string
control?: string
label?: string
errorText?: string
helperText?: string
}

export interface UseFieldProps {
/**
* The id of the field.
*/
id?: string
/**
* The ids of the field parts.
*/
ids?: ElementIds
/**
* Indicates whether the field is required.
*/
Expand All @@ -30,19 +45,23 @@ export const useField = (props: UseFieldProps) => {
const fieldset: UseFieldsetReturn | undefined = useFieldsetContext()

const {
ids,
disabled = Boolean(fieldset?.().disabled),
invalid = false,
readOnly = false,
required = false,
} = props

const [hasErrorText, setHasErrorText] = createSignal(false)
const [hasHelperText, setHasHelperText] = createSignal(false)

const id = props.id ?? createUniqueId()
let rootRef: HTMLDivElement | undefined
const errorTextId = `field::${id}::error-text`
const helperTextId = `field::${id}::helper-text`
const labelId = `field::${id}::label`

const rootId = ids?.control ?? `field::${id}`
const errorTextId = ids?.errorText ?? `field::${id}::error-text`
const helperTextId = ids?.helperText ?? `field::${id}::helper-text`
const labelId = ids?.label ?? `field::${id}::label`

createEffect(() => {
const rootNode = rootRef
Expand All @@ -66,6 +85,7 @@ export const useField = (props: UseFieldProps) => {

const getRootProps = () => ({
...parts.root.attrs,
id: rootId,
role: 'group',
'data-disabled': dataAttr(disabled),
'data-invalid': dataAttr(invalid),
Expand All @@ -89,8 +109,9 @@ export const useField = (props: UseFieldProps) => {
const getControlProps = () => ({
'aria-describedby': labelIds.join(' ') || undefined,
'aria-invalid': ariaAttr(invalid),
'aria-required': ariaAttr(required),
'aria-readonly': ariaAttr(readOnly),
'data-invalid': dataAttr(invalid),
'data-required': dataAttr(required),
'data-readonly': dataAttr(readOnly),
id,
required,
disabled,
Expand Down Expand Up @@ -147,7 +168,3 @@ export const useField = (props: UseFieldProps) => {
getErrorTextProps,
}))
}

type Booleanish = boolean | 'true' | 'false'
const dataAttr = (condition: boolean | undefined) => (condition ? '' : undefined) as Booleanish
const ariaAttr = (condition: boolean | undefined) => (condition ? true : undefined)
6 changes: 6 additions & 0 deletions packages/vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ description: All notable changes will be documented in this file.

- **Progress**: Update `Progress.ValueText` to render percentage as string.

### Fixed

- **Field**:
- Fixed issue where id of field parts could not be customized, breaking Zag.js composition.
- Added `data-*` attributes to control part to allow for better styling.

## [3.7.2] - 2024-07-28

### Changed
Expand Down
38 changes: 27 additions & 11 deletions packages/vue/src/components/field/use-field.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import { getWindow } from '@zag-js/dom-query'
import { ariaAttr, dataAttr, getWindow } from '@zag-js/dom-query'
import { type HTMLAttributes, computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { parts } from './field.anatomy'

export interface ElementIds {
root?: string
control?: string
label?: string
errorText?: string
helperText?: string
}

export interface UseFieldProps {
/**
* The id of the field.
*/
id?: string
/**
* The ids of the field parts.
*/
ids?: ElementIds
/**
* Indicates whether the field is required.
*/
Expand All @@ -25,17 +40,20 @@ export interface UseFieldProps {
export type UseFieldReturn = ReturnType<typeof useField>

export const useField = (props: UseFieldProps) => {
const { required, disabled, invalid, readOnly } = props
const { required, disabled, invalid, readOnly, ids } = props

const state = reactive({
hasErrorText: false,
hasHelperText: false,
})

const id = props.id ?? `field-${Math.random().toString(36).substr(2, 9)}`
const rootRef = ref(null)
const errorTextId = `field::${id}::error-text`
const helperTextId = `field::${id}::helper-text`
const labelId = `field::${id}::label`

const rootId = ids?.control ?? `field::${id}`
const errorTextId = ids?.errorText ?? `field::${id}::error-text`
const helperTextId = ids?.helperText ?? `field::${id}::helper-text`
const labelId = ids?.label ?? `field::${id}::label`

onMounted(() => {
const rootNode = rootRef.value
Expand All @@ -61,6 +79,7 @@ export const useField = (props: UseFieldProps) => {

const getRootProps = () => ({
...parts.root.attrs,
id: rootId,
role: 'group',
'data-disabled': dataAttr(disabled),
'data-invalid': dataAttr(invalid),
Expand All @@ -84,8 +103,9 @@ export const useField = (props: UseFieldProps) => {
const getControlProps = () => ({
'aria-describedby': labelIds.join(' ') || undefined,
'aria-invalid': ariaAttr(invalid),
'aria-required': ariaAttr(required),
'aria-readonly': ariaAttr(readOnly),
'data-invalid': dataAttr(invalid),
'data-required': dataAttr(required),
'data-readonly': dataAttr(readOnly),
id,
required,
disabled,
Expand Down Expand Up @@ -142,7 +162,3 @@ export const useField = (props: UseFieldProps) => {
getErrorTextProps,
}))
}

type Booleanish = boolean | 'true' | 'false'
const dataAttr = (condition: boolean | undefined) => (condition ? '' : undefined) as Booleanish
const ariaAttr = (condition: boolean | undefined) => (condition ? true : undefined)

0 comments on commit c8179a7

Please sign in to comment.