Skip to content
Draft
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
99 changes: 94 additions & 5 deletions src/css/frontend.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,115 @@
}
}

.wc-bookings-date-picker {
// Screen reader only text - visually hidden but accessible to screen readers
.screen-reader-text {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
word-wrap: normal !important;
}

fieldset.wc-bookings-date-picker {
fieldset {
position: relative;
}

&:not([data-selected-date-type="start"]) .ui-datepicker td.fully_booked_start_days:not(.selection-end-date),
&[data-selected-date-type="start"] .ui-datepicker td.fully_booked_end_days:not(.selection-start-date) {
opacity: 0.35;
opacity: 1;

span, a {
pointer-events: none;
}
}

.ui-datepicker {
td.fully_booked:not(.not_bookable_by_rules),
td.fully_booked {
opacity: 0.65;

span, a {
background-color: #000000 !important;
}
}
}

&:not([data-selected-date-type="start"]) .ui-datepicker td.fully_booked_start_days:not(.selection-end-date),
&[data-selected-date-type="start"] .ui-datepicker td.fully_booked_end_days:not(.selection-start-date),
.ui-datepicker td.fully_booked {
span, a {
background-color: #c0392b !important;
background-color: #000000 !important;
background-image: none !important;
border-color: rgba(0, 0, 0, 0.1) !important;
color: #fff !important;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
pointer-events: none;
position: relative;
}
}

// Partially available dates: show a clean diagonal split.
// - `fully_booked_start_days` (check-out only): bottom-right triangle is unavailable (dark gray).
// - `fully_booked_end_days` (check-in only): top-left triangle is unavailable (dark gray).
$partial_available: #22c55e; // Green for the available half
$partial_unavailable: #666666; // Dark gray for the unavailable half

// High specificity selector to override WooCommerce Bookings' .bookable styles
.ui-datepicker td.fully_booked_start_days,
.ui-datepicker td.fully_booked_end_days,
.ui-datepicker td.bookable.fully_booked_start_days,
.ui-datepicker td.bookable.fully_booked_end_days {
span, a {
position: relative;
color: #fff !important;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
border: none !important;
}
}

// Check-out only: top-left is green (available), bottom-right is dark gray (unavailable)
.ui-datepicker td.fully_booked_start_days,
.ui-datepicker td.bookable.fully_booked_start_days {
span, a {
background-color: transparent !important;
background-image: linear-gradient(
to bottom right,
$partial_available 0%,
$partial_available 50%,
$partial_unavailable 50%,
$partial_unavailable 100%
) !important;
}
}

// Check-in only: top-left is dark gray (unavailable), bottom-right is green (available)
.ui-datepicker td.fully_booked_end_days,
.ui-datepicker td.bookable.fully_booked_end_days {
span, a {
background-color: transparent !important;
background-image: linear-gradient(
to bottom right,
$partial_unavailable 0%,
$partial_unavailable 50%,
$partial_available 50%,
$partial_available 100%
) !important;
}
}

// For fully booked dates (unavailable for both check-in and check-out).
// Increased specificity to override the styling from the WooCommerce Bookings plugin.
.wc-bookings-booking-form .wc-bookings-date-picker {
.ui-datepicker td.fully_booked:not(.not_bookable_by_rules) {
opacity: 0.65;
span,
a {
background-color: #000000 !important;
}
}
}

Expand Down
191 changes: 191 additions & 0 deletions src/js/booking-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,158 @@ import {
);
}

// Add data attributes for easier access to date values
// This ensures screen reader text works correctly for all months
attributes['data-day'] = day;
attributes['data-month'] = month - 1; // 0-based for consistency with JS Date
attributes['data-year'] = year;

return attributes;
}
);

/**
* Add accessible text to datepicker cells.
*
* @param {jQuery} $cell - The datepicker cell element.
* @param {string} accessibleText - The text to announce to screen readers.
*/
const addAccessibleText = ($cell, accessibleText) => {
const $dayElement = $cell.find('span, a').first();
const $targetElement = $dayElement.length ? $dayElement : $cell;

// Remove any existing screen reader text to avoid duplication
$targetElement.find('.screen-reader-text').remove();

// Add screen reader text
$targetElement.append(
`<span class="screen-reader-text"> ${accessibleText}</span>`
);
};

// Shared month and day names for screen reader formatting
const MONTH_NAMES = [
__('January', 'woocommerce-accommodation-bookings'),
__('February', 'woocommerce-accommodation-bookings'),
__('March', 'woocommerce-accommodation-bookings'),
__('April', 'woocommerce-accommodation-bookings'),
__('May', 'woocommerce-accommodation-bookings'),
__('June', 'woocommerce-accommodation-bookings'),
__('July', 'woocommerce-accommodation-bookings'),
__('August', 'woocommerce-accommodation-bookings'),
__('September', 'woocommerce-accommodation-bookings'),
__('October', 'woocommerce-accommodation-bookings'),
__('November', 'woocommerce-accommodation-bookings'),
__('December', 'woocommerce-accommodation-bookings'),
];

const DAY_NAMES = [
__('Sunday', 'woocommerce-accommodation-bookings'),
__('Monday', 'woocommerce-accommodation-bookings'),
__('Tuesday', 'woocommerce-accommodation-bookings'),
__('Wednesday', 'woocommerce-accommodation-bookings'),
__('Thursday', 'woocommerce-accommodation-bookings'),
__('Friday', 'woocommerce-accommodation-bookings'),
__('Saturday', 'woocommerce-accommodation-bookings'),
];

/**
* Format date for screen reader announcement.
*
* @param {jQuery} $cell - The datepicker cell element.
* @return {string} Formatted date string.
*/
const formatDateForScreenReader = ($cell) => {
// Get day from data-day attribute or extract from text content
let day = $cell.attr('data-day');
if (!day) {
const $dayElement = $cell.find('span, a').first();
const textContent = $dayElement.length
? $dayElement.text()
: $cell.text();
day = textContent.trim().match(/^\d+/)?.[0];
}

// Get month and year from cell attributes or datepicker header
let dataMonth = $cell.attr('data-month');
let dataYear = $cell.attr('data-year');

if (dataMonth === undefined || dataYear === undefined) {
const $datepicker = $cell.closest('.ui-datepicker');
const $monthEl = $datepicker.find('.ui-datepicker-month');
const $yearEl = $datepicker.find('.ui-datepicker-year');

dataMonth = $monthEl.is('select')
? $monthEl.val()
: MONTH_NAMES.findIndex((m) =>
$monthEl.text().toLowerCase().includes(m.toLowerCase())
);

dataYear = $yearEl.is('select')
? $yearEl.val()
: $yearEl.text().trim();
}

if (
!day ||
dataMonth === undefined ||
dataMonth === null ||
dataMonth === -1 ||
!dataYear
) {
return '';
}

const date = new Date(
parseInt(dataYear, 10),
parseInt(dataMonth, 10),
parseInt(day, 10)
);
return `${MONTH_NAMES[date.getMonth()]}, ${dataYear}, ${
DAY_NAMES[date.getDay()]
},`;
};

/**
* Add accessible text to all booking date types in the form.
*
* @param {jQuery} $form - The booking form element.
*/
const addAccessibleTextToBookingDates = ($form) => {
// Add screen reader text for partially available dates (check-out only)
$form.find('.fully_booked_start_days').each(function () {
const $cell = $(this);
const formattedDate = formatDateForScreenReader($cell);
const accessibleText = `${formattedDate} ${__(
'Available for check-out only.',
'woocommerce-accommodation-bookings'
)}`;
addAccessibleText($cell, accessibleText);
});

// Add screen reader text for partially available dates (check-in only)
$form.find('.fully_booked_end_days').each(function () {
const $cell = $(this);
const formattedDate = formatDateForScreenReader($cell);
const accessibleText = `${formattedDate} ${__(
'Available for check-in only.',
'woocommerce-accommodation-bookings'
)}`;
addAccessibleText($cell, accessibleText);
});

// Add screen reader text for fully booked dates (both start and end unavailable)
$form.find('.fully_booked').each(function () {
const $cell = $(this);
const formattedDate = formatDateForScreenReader($cell);
const accessibleText = `${formattedDate} ${__(
'Fully booked and unavailable.',
'woocommerce-accommodation-bookings'
)}`;
addAccessibleText($cell, accessibleText);
});
};

// Make the days disable and unselectable according to the selection.
HookApi.addAction(
'wc_bookings_date_picker_refreshed',
Expand All @@ -93,6 +241,9 @@ import {
$form
.find('.fully_booked_end_days')
.removeClass('ui-datepicker-unselectable ui-state-disabled');

// Add screen reader text for all booking date types
addAccessibleTextToBookingDates($form);
}
);

Expand Down Expand Up @@ -131,6 +282,11 @@ import {
}

$fieldset.attr('data-content', data_content);

// Re-add screen reader text after date selection triggers refresh
setTimeout(() => {
addAccessibleTextToBookingDates($form);
}, 100);
}
);

Expand Down Expand Up @@ -177,4 +333,39 @@ import {
}
}
);

// Listen for datepicker month navigation (prev/next buttons and month/year dropdowns)
// This ensures screen reader text is added when navigating to different months
$(document).on(
'click',
'.ui-datepicker-prev, .ui-datepicker-next',
function () {
// Wait for datepicker to update before adding screen reader text
setTimeout(() => {
$('.product-type-accommodation-booking form').each(function () {
const $form = $(this);
if (is_product_type_accommodation_booking($form)) {
addAccessibleTextToBookingDates($form);
}
});
}, 150);
}
);

// Also handle month/year dropdown changes
$(document).on(
'change',
'.ui-datepicker-month, .ui-datepicker-year',
function () {
// Wait for datepicker to update before adding screen reader text
setTimeout(() => {
$('.product-type-accommodation-booking form').each(function () {
const $form = $(this);
if (is_product_type_accommodation_booking($form)) {
addAccessibleTextToBookingDates($form);
}
});
}, 150);
}
);
})(jQuery);
Loading