Skip to content

Commit

Permalink
feat: build a control btn util
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Feb 8, 2025
1 parent f01a5ad commit b312ee2
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 79 deletions.
1 change: 1 addition & 0 deletions packages/core/src/helpers/useControlButtonProps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useControlButtonProps';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { computed, shallowRef } from 'vue';
import { isButtonElement, withRefCapture } from '../../utils/common';

interface ControlButtonProps {
[key: string]: unknown;
disabled?: boolean;
}

export function useControlButtonProps(props: ControlButtonProps) {
const buttonEl = shallowRef<HTMLElement>();

const buttonProps = computed(() => {
const isBtn = isButtonElement(buttonEl.value);
const { disabled, ...rest } = props;

return withRefCapture(
{
type: isBtn ? ('button' as const) : undefined,
role: isBtn ? undefined : 'button',
[isBtn ? 'disabled' : 'aria-disabled']: disabled || undefined,
tabindex: '-1',
...rest,
},
buttonEl,
);
});

return buttonProps;
}
104 changes: 43 additions & 61 deletions packages/core/src/useCalendar/useCalendar.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { computed, InjectionKey, MaybeRefOrGetter, nextTick, provide, ref, Ref, shallowRef, toValue, watch } from 'vue';
import { Temporal } from '@js-temporal/polyfill';
import { CalendarDay, CalendarIdentifier } from './types';
import { hasKeyCode, isButtonElement, normalizeProps, useUniqId, withRefCapture } from '../utils/common';
import { hasKeyCode, normalizeProps, useUniqId, withRefCapture } from '../utils/common';
import { Reactivify } from '../types';
import { useDateFormatter, useLocale } from '../i18n';
import { WeekInfo } from '../i18n/getWeekInfo';
import { FieldTypePrefixes } from '../constants';
import { usePopoverController } from '../helpers/usePopoverController';
import { blockEvent } from '../utils/events';
import { useLabel } from '../a11y';
import { useControlButtonProps } from '../helpers/useControlButtonProps';

export interface CalendarProps {
/**
Expand Down Expand Up @@ -44,6 +46,11 @@ export interface CalendarProps {
* The label for the previous month button.
*/
previousMonthButtonLabel?: string;

/**
* The format options for the month and year label.
*/
monthYearFormatOptions?: Intl.DateTimeFormatOptions;
}

interface CalendarContext {
Expand All @@ -61,15 +68,18 @@ export const CalendarContextKey: InjectionKey<CalendarContext> = Symbol('Calenda
export function useCalendar(_props: Reactivify<CalendarProps, 'onDaySelected'> = {}) {
const props = normalizeProps(_props, ['onDaySelected']);
const calendarId = useUniqId(FieldTypePrefixes.Calendar);
const gridId = `${calendarId}-g`;
const pickerEl = ref<HTMLElement>();
const gridEl = ref<HTMLElement>();
const buttonEl = ref<HTMLElement>();
const calendarLabelEl = ref<HTMLElement>();
const { weekInfo, locale, calendar } = useLocale(props.locale, {
calendar: () => toValue(props.calendar),
});

const formatter = useDateFormatter(locale, { month: 'long', year: 'numeric' });
const formatter = useDateFormatter(
locale,
() => toValue(props.monthYearFormatOptions) ?? { month: 'long', year: 'numeric' },
);
const selectedDate = computed(() => toValue(props.currentDate) ?? Temporal.Now.zonedDateTime(calendar.value));
const focusedDay = shallowRef<Temporal.ZonedDateTime>();
const { isOpen } = usePopoverController(pickerEl, { disabled: props.disabled });
Expand Down Expand Up @@ -102,20 +112,10 @@ export function useCalendar(_props: Reactivify<CalendarProps, 'onDaySelected'> =
const { daysOfTheWeek } = useDaysOfTheWeek(context);
const { days } = useCalendarDays(context);

const buttonProps = computed(() => {
const isBtn = isButtonElement(buttonEl.value);

return withRefCapture(
{
type: isBtn ? ('button' as const) : undefined,
role: isBtn ? undefined : 'button',
tabindex: '-1',
onClick: () => {
isOpen.value = true;
},
},
buttonEl,
);
const buttonProps = useControlButtonProps({
onClick: () => {
isOpen.value = true;
},
});

const pickerHandlers = {
Expand Down Expand Up @@ -170,68 +170,50 @@ export function useCalendar(_props: Reactivify<CalendarProps, 'onDaySelected'> =
);
});

const gridProps = computed(() => {
return withRefCapture(
{
id: `${calendarId}-g`,
role: 'grid',
},
gridEl,
);
const nextMonthButtonProps = useControlButtonProps({
id: `${calendarId}-next-month`,
onClick: () => {
context.setFocusedDay(context.getFocusedDate().add({ months: 1 }));
},
});

const monthYearLabelProps = computed(() => {
return withRefCapture(
{
id: `${calendarId}-label`,
'aria-live': 'polite' as const,
},
calendarLabelEl,
);
const previousMonthButtonProps = useControlButtonProps({
id: `${calendarId}-previous-month`,
onClick: () => {
context.setFocusedDay(context.getFocusedDate().subtract({ months: 1 }));
},
});

const nextMonthBtn = ref<HTMLElement>();
const monthYearLabel = computed(() => {
return formatter.value.format(context.getFocusedDate().toPlainDateTime());
});

const nextMonthButtonProps = computed(() => {
const isBtn = isButtonElement(nextMonthBtn.value);
const { labelProps: monthYearLabelBaseProps } = useLabel({
targetRef: gridEl,
for: gridId,
label: monthYearLabel,
});

const monthYearLabelProps = computed(() => {
return withRefCapture(
{
id: `${calendarId}-next-month`,
type: isBtn ? ('button' as const) : undefined,
role: isBtn ? undefined : 'button',
tabindex: '-1',
onClick: () => {
context.setFocusedDay(context.getFocusedDate().add({ months: 1 }));
},
...monthYearLabelBaseProps.value,
'aria-live': 'polite' as const,
},
nextMonthBtn,
calendarLabelEl,
);
});

const previousMonthBtn = ref<HTMLElement>();

const previousMonthButtonProps = computed(() => {
const isBtn = isButtonElement(previousMonthBtn.value);

const gridProps = computed(() => {
return withRefCapture(
{
id: `${calendarId}-previous-month`,
type: isBtn ? ('button' as const) : undefined,
role: isBtn ? undefined : 'button',
tabindex: '-1',
onClick: () => {
context.setFocusedDay(context.getFocusedDate().subtract({ months: 1 }));
},
id: `${calendarId}-g`,
role: 'grid',
},
previousMonthBtn,
gridEl,
);
});

const monthYearLabel = computed(() => {
return formatter.value.format(context.getFocusedDate().toPlainDateTime());
});

provide(CalendarContextKey, context);

return {
Expand Down
28 changes: 10 additions & 18 deletions packages/core/src/useComboBox/useComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useListBox } from '../useListBox';
import { useErrorMessage } from '../a11y/useErrorMessage';
import { useInputValidity } from '../validation';
import { FilterFn } from '../collections';
import { useControlButtonProps } from '../helpers/useControlButtonProps';

export interface ComboBoxProps<TOption, TValue = TOption> {
/**
Expand Down Expand Up @@ -315,24 +316,15 @@ export function useComboBox<TOption, TValue = TOption>(
isPopupOpen.value = !isPopupOpen.value;
}

const buttonProps = computed(() => {
const isButton = buttonEl.value?.tagName === 'BUTTON';

return withRefCapture(
{
id: inputId,
role: isButton ? undefined : 'button',
[isButton ? 'disabled' : 'aria-disabled']: isDisabled.value || undefined,
tabindex: '-1',
type: 'button' as const,
'aria-haspopup': 'listbox' as const,
'aria-expanded': isPopupOpen.value,
'aria-activedescendant': findFocusedOption()?.id ?? undefined,
'aria-controls': listBoxId,
onClick: onButtonClick,
},
buttonEl,
);
const buttonProps = useControlButtonProps({
id: `${inputId}-btn`,
disabled: isDisabled.value,
type: 'button' as const,
'aria-haspopup': 'listbox' as const,
'aria-expanded': isPopupOpen.value,
'aria-activedescendant': findFocusedOption()?.id ?? undefined,
'aria-controls': listBoxId,
onClick: onButtonClick,
});

// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/#rps_label_textbox
Expand Down

0 comments on commit b312ee2

Please sign in to comment.