Skip to content

Commit

Permalink
feat(core): add missing label on inline datepicker (closes #523)
Browse files Browse the repository at this point in the history
  • Loading branch information
fynnfeldpausch committed May 29, 2024
1 parent ae6811e commit 828f7b3
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 12 deletions.
45 changes: 45 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,27 @@ export namespace Components {
* Clear the picker.
*/
"clear": () => Promise<void>;
/**
* Programmatically move focus to the inline datepicker, i,e, the first focusable date.
* @param options An optional object providing options to control aspects of the focusing process.
*/
"doFocus": (options?: FocusOptions) => Promise<void>;
/**
* Shows an arrow keys navigation hint.
*/
"hint": boolean;
/**
* A unique identifier for the input.
*/
"identifier"?: string;
/**
* The label for the input.
*/
"label": string;
/**
* Visually hide the label, but still show it to assistive technologies like screen readers.
*/
"labelHidden": boolean;
/**
* A maximum value for the date, given in local ISO 8601 date format YYYY-MM-DD.
*/
Expand All @@ -445,6 +462,14 @@ export namespace Components {
* Allow the selection of a range of dates, i.e. start and end date.
*/
"range": boolean;
/**
* A value is required or must be check for the form to be submittable.
*/
"required": boolean;
/**
* Whether the label need a marker to shown if the input is required or optional.
*/
"requiredMarker"?: 'none' | 'required' | 'optional' | 'none!' | 'optional!' | 'required!';
/**
* Resets the view of the picker.
*/
Expand Down Expand Up @@ -2560,6 +2585,18 @@ declare namespace LocalJSX {
* Shows an arrow keys navigation hint.
*/
"hint"?: boolean;
/**
* A unique identifier for the input.
*/
"identifier"?: string;
/**
* The label for the input.
*/
"label"?: string;
/**
* Visually hide the label, but still show it to assistive technologies like screen readers.
*/
"labelHidden"?: boolean;
/**
* A maximum value for the date, given in local ISO 8601 date format YYYY-MM-DD.
*/
Expand All @@ -2584,6 +2621,14 @@ declare namespace LocalJSX {
* Allow the selection of a range of dates, i.e. start and end date.
*/
"range"?: boolean;
/**
* A value is required or must be check for the form to be submittable.
*/
"required"?: boolean;
/**
* Whether the label need a marker to shown if the input is required or optional.
*/
"requiredMarker"?: 'none' | 'required' | 'optional' | 'none!' | 'optional!' | 'required!';
/**
* The value of the control, given in local ISO 8601 date format YYYY-MM-DD.
*/
Expand Down
9 changes: 8 additions & 1 deletion core/src/components/cat-date-inline/cat-date-inline.scss
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
@use 'variables' as *;
@use 'mixins' as *;
@use '_snippets/form-label';

:host {
display: block;
display: flex;
flex-direction: column;
gap: 0.5rem;
}

:host([hidden]) {
display: none;
}

.label-container:empty {
display: none;
}

.picker {
display: flex;
flex-direction: column;
Expand Down
104 changes: 95 additions & 9 deletions core/src/components/cat-date-inline/cat-date-inline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { Component, Element, Event, EventEmitter, Host, Listen, Method, Prop, St
import { catI18nRegistry as i18n } from '../cat-i18n/cat-i18n-registry';
import { getLocale } from './cat-date-locale';
import { addDays, addMonth, clampDate, isSameDay, isSameMonth, isSameYear } from './cat-date-math';
import { delayedAssertWarn } from '../../utils/assert';
import firstTabbable from '../../utils/first-tabbable';

let nextUniqueId = 0;

/**
* An inline date picker component to select a date.
Expand All @@ -12,20 +16,34 @@ import { addDays, addMonth, clampDate, isSameDay, isSameMonth, isSameYear } from
shadow: true
})
export class CatDateInline {
private readonly _id = `cat-date-inline-${nextUniqueId++}`;
private get id() {
return this.identifier || this._id;
}

private readonly language = i18n.getLocale();
private readonly locale = getLocale(this.language);
// additonally store the focus date to ensure correct focus after potential re-render
private focusDate: Date | null = null;

@Element() hostElement!: HTMLElement;

@State() hasSlottedLabel = false;

@State() hasSlottedHint = false;

@State() viewDate: Date = this.locale.now();

/**
* Hides the clear button.
*/
@Prop() noClear = false;

/**
* A unique identifier for the input.
*/
@Prop() identifier?: string;

/**
* Shows an arrow keys navigation hint.
*/
Expand All @@ -41,6 +59,16 @@ export class CatDateInline {
*/
@Prop() weeks = false;

/**
* The label for the input.
*/
@Prop() label = '';

/**
* Visually hide the label, but still show it to assistive technologies like screen readers.
*/
@Prop() labelHidden = false;

/**
* A minimum value for the date, given in local ISO 8601 date format YYYY-MM-DD.
*/
Expand All @@ -56,6 +84,16 @@ export class CatDateInline {
*/
@Prop() range = false;

/**
* A value is required or must be check for the form to be submittable.
*/
@Prop() required = false;

/**
* Whether the label need a marker to shown if the input is required or optional.
*/
@Prop() requiredMarker?: 'none' | 'required' | 'optional' | 'none!' | 'optional!' | 'required!' = 'optional';

/**
* The value of the control, given in local ISO 8601 date format YYYY-MM-DD.
*/
Expand All @@ -77,12 +115,24 @@ export class CatDateInline {
componentWillLoad() {
const [startDate, endDate] = this.getValue();
if (endDate) {
this.focus(endDate);
this.focus(endDate, false);
} else if (startDate) {
this.focus(startDate);
this.focus(startDate, false);
}
}

componentWillRender(): void {
delayedAssertWarn(
this,
() => {
this.hasSlottedLabel = !!this.hostElement.querySelector('[slot="label"]');
this.hasSlottedHint = !!this.hostElement.querySelector('[slot="hint"]');
return !!this.label && !!this.hasSlottedLabel;
},
'[A11y] Missing ARIA label on input'
);
}

componentDidRender() {
if (this.focusDate) {
// re-focus the previously focused date after re-render
Expand Down Expand Up @@ -171,13 +221,46 @@ export class CatDateInline {
this.viewDate = dateStart ?? clampDate(minDate, this.locale.now(), maxDate);
}

/**
* Programmatically move focus to the inline datepicker, i,e, the first
* focusable date.
*
* @param options An optional object providing options to control aspects of
* the focusing process.
*/
@Method()
async doFocus(options?: FocusOptions): Promise<void> {
firstTabbable(this.hostElement.shadowRoot?.querySelector('.picker-grid-days'))?.focus(options);
}

render() {
const [minDate, maxDate] = this.getMinMaxDate();
const dateGrid = this.dateGrid(this.viewDate.getFullYear(), this.viewDate.getMonth());
const [dateStart, dateEnd] = this.getValue();
return (
<Host>
<div class={{ picker: true, 'picker-weeks': this.weeks }}>
<div class={{ 'label-container': true, hidden: this.labelHidden }}>
{(this.hasSlottedLabel || this.label) && (
<label id={`${this.id}-label`} htmlFor={this.id} part="label" onClick={() => this.doFocus()}>
<span class="label-wrapper">
{(this.hasSlottedLabel && <slot name="label"></slot>) || this.label}
<div class="label-metadata">
{!this.required && (this.requiredMarker ?? 'optional').startsWith('optional') && (
<span class="label-optional" aria-hidden="true">
({i18n.t('input.optional')})
</span>
)}
{this.required && this.requiredMarker?.startsWith('required') && (
<span class="label-optional" aria-hidden="true">
({i18n.t('input.required')})
</span>
)}
</div>
</span>
</label>
)}
</div>
<div class={{ picker: true, 'picker-weeks': this.weeks }} id={this.id} aria-describedby={`${this.id}-label`}>
<div class="picker-head">
<cat-button
icon="$cat:datepicker-year-prev"
Expand Down Expand Up @@ -291,13 +374,16 @@ export class CatDateInline {
);
}

private focus(date: Date) {
private focus(date: Date, focus = true) {
const [minDate, maxDate] = this.getMinMaxDate();
this.focusDate = clampDate(minDate, date, maxDate);
this.viewDate = new Date(this.focusDate.getFullYear(), this.focusDate.getMonth());
this.hostElement.shadowRoot
?.querySelector<HTMLCatButtonElement>(`[data-date="${this.locale.toLocalISO(this.focusDate)}"]`)
?.doFocus();
const focusDate = clampDate(minDate, date, maxDate);
this.viewDate = new Date(focusDate.getFullYear(), focusDate.getMonth());
if (focus) {
this.focusDate = focusDate;
this.hostElement.shadowRoot
?.querySelector<HTMLCatButtonElement>(`[data-date="${this.locale.toLocalISO(focusDate)}"]`)
?.doFocus();
}
}

private navigate(direction: 'prev' | 'next', period: 'year' | 'month') {
Expand Down
4 changes: 3 additions & 1 deletion core/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -919,10 +919,12 @@ <h2>Datepicker</h2>
<cat-datepicker mode="time" label="Time"></cat-datepicker>
<cat-datepicker mode="week" label="Week"></cat-datepicker>
<cat-datepicker mode="daterange" label="Daterange"></cat-datepicker>
<h3>Inline Datepicker (new)</h3>
<cat-date-inline value="2022-06-21" label="test this is"></cat-date-inline>
<h3>Inline Datepicker</h3>
<cat-card class="cat-inline-flex cat-flex-col cat-border">
<h4>Datetime</h4>
<cat-datepicker-inline value="2022-06-21T00:00:00Z"></cat-datepicker-inline>
<cat-datepicker-inline value="2022-06-21T00:00:000Z"></cat-datepicker-inline>
</cat-card>
<h3>Nested Datepicker</h3>
<cat-dropdown>
Expand Down
2 changes: 1 addition & 1 deletion core/src/utils/assert.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import log from 'loglevel';

export function delayedAssertWarn(component: unknown, assertion: () => boolean, message: string, timeout = 500): void {
setTimeout(() => {
window.setTimeout(() => {
if (!assertion()) {
log.warn(message, component);
}
Expand Down

0 comments on commit 828f7b3

Please sign in to comment.