Skip to content

Commit

Permalink
fix(Dates): disable state not affect tabindex and relevant button (#1146
Browse files Browse the repository at this point in the history
)

* fix(Dates): disable state not affect tabindex and relevant button

* test: add test for disable props
  • Loading branch information
zernonia authored Jul 19, 2024
1 parent 2619a0f commit e3bc0cc
Show file tree
Hide file tree
Showing 17 changed files with 132 additions and 34 deletions.
7 changes: 7 additions & 0 deletions packages/radix-vue/src/Calendar/Calendar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,10 +508,12 @@ describe('calendar', async () => {
const firstDayOfMonth = getByTestId('date-1-1')
expect(firstDayOfMonth).toHaveAttribute('aria-disabled', 'true')
expect(firstDayOfMonth).toHaveAttribute('data-disabled')

await user.click(firstDayOfMonth)
expect(firstDayOfMonth).not.toHaveAttribute('data-selected')
firstDayOfMonth.focus()
expect(firstDayOfMonth).not.toHaveFocus()
expect(firstDayOfMonth).not.toHaveAttribute('tabindex')

const tenthDayOfMonth = getByTestId('date-1-10')
expect(tenthDayOfMonth).toHaveAttribute('aria-disabled', 'true')
Expand All @@ -520,6 +522,11 @@ describe('calendar', async () => {
expect(tenthDayOfMonth).not.toHaveAttribute('data-selected')
tenthDayOfMonth.focus()
expect(tenthDayOfMonth).not.toHaveFocus()

const prevButton = getByTestId('prev-button')
const nextButton = getByTestId('next-button')
expect(prevButton).toBeDisabled()
expect(nextButton).toBeDisabled()
})

it('prevents selection but allows focus when `readonly` is `true`', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/radix-vue/src/Calendar/CalendarCellTrigger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const isOutsideVisibleView = computed(() =>
)
const isFocusedDate = computed(() => {
return isSameDay(props.day, rootContext.placeholder.value)
return !rootContext.disabled.value && isSameDay(props.day, rootContext.placeholder.value)
})
const isSelectedDate = computed(() => rootContext.isDateSelected(props.day))
Expand Down
8 changes: 5 additions & 3 deletions packages/radix-vue/src/Calendar/CalendarNext.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ export interface CalendarNextProps extends PrimitiveProps {
</script>

<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from '@/Primitive'
import { injectCalendarRootContext } from './CalendarRoot.vue'
const props = withDefaults(defineProps<CalendarNextProps>(), { as: 'button', step: 'month' })
const disabled = computed(() => rootContext.disabled.value || rootContext.isNextButtonDisabled(props.step, props.nextPage))
const rootContext = injectCalendarRootContext()
</script>
Expand All @@ -29,9 +31,9 @@ const rootContext = injectCalendarRootContext()
:as-child="props.asChild"
aria-label="Next page"
:type="as === 'button' ? 'button' : undefined"
:aria-disabled="rootContext.isNextButtonDisabled(props.step, props.nextPage) || undefined"
:data-disabled="rootContext.isNextButtonDisabled(props.step, props.nextPage) || undefined"
:disabled="rootContext.isNextButtonDisabled(props.step, props.nextPage)"
:aria-disabled="disabled || undefined"
:data-disabled="disabled || undefined"
:disabled="disabled"
@click="rootContext.nextPage(props.step, props.nextPage)"
>
<slot>Next page</slot>
Expand Down
8 changes: 5 additions & 3 deletions packages/radix-vue/src/Calendar/CalendarPrev.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ export interface CalendarPrevProps extends PrimitiveProps {
</script>

<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from '@/Primitive'
import { injectCalendarRootContext } from './CalendarRoot.vue'
const props = withDefaults(defineProps<CalendarPrevProps>(), { as: 'button', step: 'month' })
const disabled = computed(() => rootContext.disabled.value || rootContext.isPrevButtonDisabled(props.step, props.prevPage))
const rootContext = injectCalendarRootContext()
</script>
Expand All @@ -29,9 +31,9 @@ const rootContext = injectCalendarRootContext()
:as="props.as"
:as-child="props.asChild"
:type="as === 'button' ? 'button' : undefined"
:aria-disabled="rootContext.isPrevButtonDisabled(props.step, props.prevPage) || undefined"
:data-disabled="rootContext.isPrevButtonDisabled(props.step, props.prevPage) || undefined"
:disabled="rootContext.isPrevButtonDisabled(props.step, props.prevPage)"
:aria-disabled="disabled || undefined"
:data-disabled="disabled || undefined"
:disabled="disabled"
@click="rootContext.prevPage(props.step, props.prevPage)"
>
<slot>Prev page</slot>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ function paging(date: DateValue, sign: -1 | 1) {
<Calendar :default-value="defaultValue" />
</Variant>

<Variant title="Disabled">
<Calendar :disabled="true" />
</Variant>

<Variant title="Fixed weeks">
<Calendar
:default-value="defaultValue"
Expand Down
1 change: 1 addition & 0 deletions packages/radix-vue/src/DateField/DateField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ describe('dateField', async () => {
for (const seg of segments) {
await user.click(seg)
expect(seg).not.toHaveFocus()
expect(seg).not.toHaveAttribute('tabindex')
}
})

Expand Down
1 change: 0 additions & 1 deletion packages/radix-vue/src/DateField/DateFieldRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,6 @@ defineExpose({
:dir="dir"
@keydown.left.right="handleKeydown"
>
{{ currentSegmentIndex }}
<slot
:model-value="modelValue"
:segments="segmentContents"
Expand Down
42 changes: 23 additions & 19 deletions packages/radix-vue/src/DateField/useDateField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,24 @@ type DateTimeValueIncrementation = {
}

type SegmentAttrProps = {
disabled: boolean
segmentValues: SegmentValueObj
hourCycle: HourCycle
placeholder: DateValue
formatter: Formatter
}

const defaultSegmentAttrs = {
role: 'spinbutton',
contenteditable: true,
tabindex: 0,
spellcheck: false,
inputmode: 'numeric',
autocorrect: 'off',
enterkeyhint: 'next',
style: 'caret-color: transparent;',
function commonSegmentAttrs(props: SegmentAttrProps) {
return {
role: 'spinbutton',
contenteditable: true,
tabindex: props.disabled ? undefined : 0,
spellcheck: false,
inputmode: 'numeric',
autocorrect: 'off',
enterkeyhint: 'next',
style: 'caret-color: transparent;',
}
}

function daySegmentAttrs(props: SegmentAttrProps) {
Expand All @@ -49,7 +52,7 @@ function daySegmentAttrs(props: SegmentAttrProps) {
const valueText = isEmpty ? 'Empty' : `${valueNow}`

return {
...defaultSegmentAttrs,
...commonSegmentAttrs(props),
'aria-label': 'day,',
'aria-valuemin': valueMin,
'aria-valuemax': valueMax,
Expand All @@ -71,7 +74,7 @@ function monthSegmentAttrs(props: SegmentAttrProps) {
const valueText = isEmpty ? 'Empty' : `${valueNow} - ${formatter.fullMonth(toDate(date))}`

return {
...defaultSegmentAttrs,
...commonSegmentAttrs(props),
'aria-label': 'month, ',
'contenteditable': true,
'aria-valuemin': valueMin,
Expand All @@ -92,7 +95,7 @@ function yearSegmentAttrs(props: SegmentAttrProps) {
const valueText = isEmpty ? 'Empty' : `${valueNow}`

return {
...defaultSegmentAttrs,
...commonSegmentAttrs(props),
'aria-label': 'year, ',
'aria-valuemin': valueMin,
'aria-valuemax': valueMax,
Expand All @@ -115,7 +118,7 @@ function hourSegmentAttrs(props: SegmentAttrProps) {
const valueText = isEmpty ? 'Empty' : `${valueNow} ${segmentValues.dayPeriod ?? ''}`

return {
...defaultSegmentAttrs,
...commonSegmentAttrs(props),
'aria-label': 'hour, ',
'aria-valuemin': valueMin,
'aria-valuemax': valueMax,
Expand All @@ -139,7 +142,7 @@ function minuteSegmentAttrs(props: SegmentAttrProps) {
const valueText = isEmpty ? 'Empty' : `${valueNow}`

return {
...defaultSegmentAttrs,
...commonSegmentAttrs(props),
'aria-label': 'minute, ',
'aria-valuemin': valueMin,
'aria-valuemax': valueMax,
Expand All @@ -163,7 +166,7 @@ function secondSegmentAttrs(props: SegmentAttrProps) {
const valueText = isEmpty ? 'Empty' : `${valueNow}`

return {
...defaultSegmentAttrs,
...commonSegmentAttrs(props),
'aria-label': 'second, ',
'aria-valuemin': valueMin,
'aria-valuemax': valueMax,
Expand All @@ -184,7 +187,7 @@ function dayPeriodSegmentAttrs(props: SegmentAttrProps) {
const valueText = segmentValues.dayPeriod ?? 'AM'

return {
...defaultSegmentAttrs,
...commonSegmentAttrs(props),
'inputmode': 'text',
'aria-label': 'AM/PM',
'aria-valuemin': valueMin,
Expand All @@ -194,20 +197,20 @@ function dayPeriodSegmentAttrs(props: SegmentAttrProps) {
}
}

function literalSegmentAttrs(_: SegmentAttrProps) {
function literalSegmentAttrs(_props: SegmentAttrProps) {
return {
'aria-hidden': true,
'data-segment': 'literal',
}
}

function timeZoneSegmentAttrs(_: SegmentAttrProps) {
function timeZoneSegmentAttrs(props: SegmentAttrProps) {
return {
'role': 'textbox',
'aria-label': 'timezone, ',
'data-readonly': true,
'data-segment': 'timeZoneName',
'tabindex': 0,
'tabindex': props.disabled ? undefined : 0,
'style': 'caret-color: transparent;',
}
}
Expand Down Expand Up @@ -565,6 +568,7 @@ export function useDateField(props: UseDateFieldProps) {
}

const attributes = computed(() => segmentBuilders[props.part].attrs({
disabled: props.disabled.value,
placeholder: props.placeholder.value,
hourCycle: props.hourCycle,
segmentValues: props.segmentValues.value,
Expand Down
12 changes: 12 additions & 0 deletions packages/radix-vue/src/DatePicker/DatePicker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,16 @@ describe('datePicker', async () => {
expect(seg).toHaveFocus()
}
})

it('prevents interaction and picker to be opened when `disabled` is `true`', async () => {
const { trigger, day, month, year } = setup({
datePickerProps: {
disabled: true,
},
})
expect(trigger).toBeDisabled()
expect(day).not.toHaveAttribute('tabindex')
expect(month).not.toHaveAttribute('tabindex')
expect(year).not.toHaveAttribute('tabindex')
})
})
1 change: 1 addition & 0 deletions packages/radix-vue/src/DatePicker/DatePickerTrigger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const rootContext = injectDatePickerRootContext()
<PopoverTrigger
data-radix-vue-date-field-segment="trigger"
v-bind="props"
:disabled="rootContext.disabled.value"
@focusin="(e: FocusEvent) => {
rootContext.dateFieldRef.value?.setFocusedElement(e.target as HTMLElement)
}"
Expand Down
21 changes: 21 additions & 0 deletions packages/radix-vue/src/DateRangePicker/DateRangePicker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,25 @@ describe('dateRangePicker', async () => {
}
}
})

it('prevents interaction and picker to be opened when `disabled` is `true`', async () => {
const { getByTestId, trigger } = setup({
dateFieldProps: {
disabled: true,
},
})
expect(trigger).toBeDisabled()

const fields = ['end', 'start'] as const
const segments = ['year', 'day', 'month'] as const

for (const field of fields) {
for (const segment of segments) {
if (field === 'end' && segment === 'year')
continue
const seg = getByTestId(`${field}-${segment}`)
expect(seg).not.toHaveAttribute('tabindex')
}
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const rootContext = injectDateRangePickerRootContext()
<PopoverTrigger
data-radix-vue-date-field-segment="trigger"
v-bind="props"
:disabled="rootContext.disabled.value"
@focusin="(e: FocusEvent) => {
rootContext.dateFieldRef.value?.setFocusedElement(e.target as HTMLElement)
}"
Expand Down
36 changes: 36 additions & 0 deletions packages/radix-vue/src/RangeCalendar/RangeCalendar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,42 @@ describe('rangeCalendar', () => {
expect(heading).toHaveTextContent('March 1980')
})

it('doesnt allow focus or interaction when `disabled` is `true`', async () => {
const { getByTestId, user } = setup({
calendarProps: {
modelValue: calendarDateRange,
disabled: true,
},
})

const grid = getByTestId('grid-1')
expect(grid).toHaveAttribute('aria-disabled', 'true')
expect(grid).toHaveAttribute('data-disabled')

const firstDayOfMonth = getByTestId('date-1-1')
expect(firstDayOfMonth).toHaveAttribute('aria-disabled', 'true')
expect(firstDayOfMonth).toHaveAttribute('data-disabled')

await user.click(firstDayOfMonth)
expect(firstDayOfMonth).not.toHaveAttribute('data-selected')
firstDayOfMonth.focus()
expect(firstDayOfMonth).not.toHaveFocus()
expect(firstDayOfMonth).not.toHaveAttribute('tabindex')

const tenthDayOfMonth = getByTestId('date-1-10')
expect(tenthDayOfMonth).toHaveAttribute('aria-disabled', 'true')
expect(tenthDayOfMonth).toHaveAttribute('data-disabled')
await user.click(tenthDayOfMonth)
expect(tenthDayOfMonth).not.toHaveAttribute('data-selected')
tenthDayOfMonth.focus()
expect(tenthDayOfMonth).not.toHaveFocus()

const prevButton = getByTestId('prev-button')
const nextButton = getByTestId('next-button')
expect(prevButton).toBeDisabled()
expect(nextButton).toBeDisabled()
})

it('does not navigate after `maxValue` (with keyboard)', async () => {
const { getByTestId, user } = setup({
calendarProps: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const isOutsideVisibleView = computed(() =>
const dayValue = computed(() => props.day.day.toLocaleString(rootContext.locale.value))
const isFocusedDate = computed(() => {
return isSameDay(props.day, rootContext.placeholder.value)
return !rootContext.disabled.value && isSameDay(props.day, rootContext.placeholder.value)
})
function changeDate(date: DateValue) {
Expand Down
8 changes: 5 additions & 3 deletions packages/radix-vue/src/RangeCalendar/RangeCalendarNext.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ export interface RangeCalendarNextProps extends PrimitiveProps {
</script>

<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from '@/Primitive'
import { injectRangeCalendarRootContext } from './RangeCalendarRoot.vue'
const props = withDefaults(defineProps<RangeCalendarNextProps>(), { as: 'button' })
const disabled = computed(() => rootContext.disabled.value || rootContext.isNextButtonDisabled(props.step, props.nextPage))
const rootContext = injectRangeCalendarRootContext()
</script>
Expand All @@ -28,9 +30,9 @@ const rootContext = injectRangeCalendarRootContext()
v-bind="props"
aria-label="Next page"
:type="as === 'button' ? 'button' : undefined"
:aria-disabled="rootContext.isNextButtonDisabled(props.step, props.nextPage) || undefined"
:data-disabled="rootContext.isNextButtonDisabled(props.step, props.nextPage) || undefined"
:disabled="rootContext.isNextButtonDisabled(props.step, props.nextPage)"
:aria-disabled="disabled || undefined"
:data-disabled="disabled || undefined"
:disabled="disabled"
@click="rootContext.nextPage(props.step, props.nextPage)"
>
<slot>Next page</slot>
Expand Down
Loading

0 comments on commit e3bc0cc

Please sign in to comment.