Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading