Skip to content

Commit

Permalink
Datepicker and Dropdown a11y fixes (#1916)
Browse files Browse the repository at this point in the history
* Fix some a11y regressions in Datepicker

* Fix a11y regressions in Dropdown

* Create late-baboons-dream.md

* Fix datepicker copy/paste

* Remove console.log
  • Loading branch information
splashdust authored Jan 14, 2025
1 parent 3331d19 commit 93bac39
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/late-baboons-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sebgroup/green-core': patch
---

**Datepicker and Dropdown:** minor a11y improvements
2 changes: 1 addition & 1 deletion libs/core/src/components/datepicker/datepicker.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const styles = css`
line-height: 1;
text-align: center;
&:focus-visible {
&:focus {
background-color: var(--gds-color-l3-background-primary);
color: var(--gds-color-l3-content-primary);
}
Expand Down
26 changes: 26 additions & 0 deletions libs/core/src/components/datepicker/datepicker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,4 +576,30 @@ describe('<gds-datepicker>', () => {
await expect(yearDropdown.value).to.equal('2014')
})
})

describe('Accessibility', () => {
it('should pass axe smoketest', async () => {
const el = await fixture<GdsDatepicker>(
html`<gds-datepicker
value="2014-01-10"
min="2014-01-01"
max="2034-12-31"
open
></gds-datepicker>`,
)
await el.updateComplete
await expect(el).to.be.accessible({
ignoredRules: ['color-contrast'],
})
})

it('should have a label for #spinner-0', async () => {
const el = await fixture<GdsDatepicker>(
html`<gds-datepicker label="Date"></gds-datepicker>`,
)
const label =
el.shadowRoot!.querySelector<HTMLLabelElement>('[for="spinner-0"]')!
expect(label).to.exist
})
})
})
33 changes: 19 additions & 14 deletions libs/core/src/components/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { localized, msg } from '@lit/localize'
import { nothing } from 'lit'
import { property, query, queryAll, queryAsync, state } from 'lit/decorators.js'
import { classMap } from 'lit/directives/class-map.js'
import { join } from 'lit/directives/join.js'
import { map } from 'lit/directives/map.js'
import { repeat } from 'lit/directives/repeat.js'
Expand Down Expand Up @@ -158,7 +157,7 @@ export class GdsDatepicker extends GdsFormControlElement<Date> {
* Get a string representation of the currently displayed value in the input field. The formatting will match the dateformat attribute.
*/
get displayValue() {
return this._elInput.innerText.replace(/\s+/g, '')
return this._elField.innerText.replace(/\s+/g, '')
}

/**
Expand Down Expand Up @@ -191,14 +190,14 @@ export class GdsDatepicker extends GdsFormControlElement<Date> {
@queryAsync('#calendar-button')
private _elTrigger!: Promise<HTMLButtonElement>

@queryAsync('#date-picker')
private _elField!: Promise<HTMLDivElement>
@queryAsync('#field')
private _elFieldAsync!: Promise<HTMLDivElement>

@queryAll('[role=spinbutton]')
private _elSpinners!: NodeListOf<GdsDatePartSpinner>

@query('.input')
private _elInput!: HTMLDivElement
@query('#field')
private _elField!: HTMLDivElement

#valueOnOpen?: Date

Expand All @@ -210,7 +209,7 @@ export class GdsDatepicker extends GdsFormControlElement<Date> {
render() {
return html`
<gds-form-control-header class="size-${this.size}">
<label for="spinner-0" slot="label">${this.label}</label>
<label id="label" for="spinner-0" slot="label">${this.label}</label>
${when(
this.supportingText.length > 0,
() =>
Expand All @@ -219,11 +218,16 @@ export class GdsDatepicker extends GdsFormControlElement<Date> {
</span>`,
)}
<slot
id="supporting-text-slot"
name="extended-supporting-text"
slot="extended-supporting-text"
></slot>
<!-- @deprecated: use 'supporting-text' slot instead. Remove in 2.0 release. -->
<slot name="sub-label" slot="supporting-text"></slot>
<slot
id="sub-label-slot"
name="sub-label"
slot="supporting-text"
></slot>
</gds-form-control-header>
<gds-field-base
.size=${this.size}
Expand All @@ -232,7 +236,7 @@ export class GdsDatepicker extends GdsFormControlElement<Date> {
@click=${this.#handleFieldClick}
@copy=${this.#handleClipboardCopy}
@paste=${this.#handleClipboardPaste}
id="date-picker"
id="field"
>
<div class="spinners">
${join(
Expand All @@ -241,13 +245,14 @@ export class GdsDatepicker extends GdsFormControlElement<Date> {
(f, i) =>
html`<gds-date-part-spinner
id="spinner-${i}"
aria-invalid="${this.invalid}"
class="spinner"
.length=${f.token === 'y' ? 4 : 2}
.value=${this.#spinnerState[f.name]}
aria-valuemin=${this.#getMinSpinnerValue(f.name)}
aria-valuemax=${this.#getMaxSpinnerValue(f.name)}
aria-label=${this.#getSpinnerLabel(f.name)}
aria-describedby="label sub-label message"
aria-describedby="label supporting-text supporting-text-slot sub-label-slot message"
data-max-width=${this.#getMaxSpinnerValue(f.name).toString()
.length}
@keydown=${this.#handleSpinnerKeydown}
Expand Down Expand Up @@ -288,7 +293,7 @@ export class GdsDatepicker extends GdsFormControlElement<Date> {
// Wrapped in a slot for backwards compatibility with the deprecated message slot
// Remove for 2.0 release
() => html`
<slot name="message" slot="message">
<slot id="message" name="message" slot="message">
<gds-icon-triangle-exclamation
solid
></gds-icon-triangle-exclamation>
Expand All @@ -300,7 +305,7 @@ export class GdsDatepicker extends GdsFormControlElement<Date> {
<gds-popover
.triggerRef=${this._elTrigger}
.anchorRef=${this._elField}
.anchorRef=${this._elFieldAsync}
.open=${this.open}
@gds-ui-state=${this.#handlePopoverStateChange}
label=${this.label}
Expand Down Expand Up @@ -558,15 +563,15 @@ export class GdsDatepicker extends GdsFormControlElement<Date> {
}

#handleClipboardCopy = (e: ClipboardEvent) => {
this._elField.then((field) => {
this._elFieldAsync.then((field) => {
if (e.currentTarget !== field) return
e.preventDefault()
e.clipboardData?.setData('text/plain', this.displayValue)
})
}

#handleClipboardPaste = (e: ClipboardEvent) => {
this._elField.then((field: HTMLElement) => {
this._elFieldAsync.then((field: HTMLElement) => {
if (e.currentTarget !== field) return
e.preventDefault()
const pasted = e.clipboardData?.getData('text/plain')
Expand Down
27 changes: 27 additions & 0 deletions libs/core/src/components/dropdown/dropdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,3 +702,30 @@ describe('<gds-dropdown multiple>', () => {
await expect(el.displayValue).to.equal('Select values')
})
})

describe('<gds-dropdown> accessibility', () => {
it('pass axe smoketest', async () => {
const el = await fixture<GdsDropdown>(html`
<gds-dropdown label="My dropdown">
<gds-option value="v1">Option 1</gds-option>
<gds-option value="v2">Option 2</gds-option>
<gds-option value="v3">Option 3</gds-option>
</gds-dropdown>
`)
await expect(el).to.be.accessible()
})

it('should have a label for the trigger', async () => {
const el = await fixture<GdsDropdown>(html`
<gds-dropdown label="My dropdown">
<gds-option value="v1">Option 1</gds-option>
<gds-option value="v2">Option 2</gds-option>
<gds-option value="v3">Option 3</gds-option>
</gds-dropdown>
`)
const trigger = el.shadowRoot!.querySelector<HTMLElement>('button')!
const label = el.shadowRoot!.querySelector<HTMLElement>('label')!

await expect(label.getAttribute('for')).to.equal(trigger.id)
})
})
28 changes: 18 additions & 10 deletions libs/core/src/components/dropdown/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ export class GdsDropdown<ValueT = any>
!this.hideLabel,
() => html`
<gds-form-control-header>
<label for="trigger" slot="label">${this.label}</label>
<label id="label" for="trigger" slot="label">${this.label}</label>
${when(
this.supportingText.length > 0,
() =>
Expand All @@ -246,11 +246,12 @@ export class GdsDropdown<ValueT = any>
</span>`,
)}
<slot
id="extended-supporting-text"
name="extended-supporting-text"
slot="extended-supporting-text"
></slot>
<!-- @deprecated: use 'supporting-text' slot instead. Remove in 2.0 release. -->
<slot name="sub-label" slot="supporting-text"></slot>
<slot id="sub-label" name="sub-label" slot="supporting-text"></slot>
</gds-form-control-header>
`,
)}
Expand All @@ -267,17 +268,24 @@ export class GdsDropdown<ValueT = any>
.size=${this.size}
.disabled=${this.disabled}
.invalid=${this.invalid}
aria-haspopup="listbox"
slot="trigger"
role="combobox"
aria-owns="listbox"
aria-controls="listbox"
aria-expanded="${this.open}"
aria-label="${this.label}"
id="field"
>
<slot name="lead" slot="lead"></slot>
<button id="trigger" name="trigger">
<button
id="trigger"
role="combobox"
aria-expanded="${this.open}"
aria-owns="listbox"
aria-haspopup="listbox"
aria-controls="listbox"
name="trigger"
aria-label="${this.label} ${this.displayValue}"
aria-describedby="supporting-text extended-supporting-text sub-label message"
aria-invalid="${this.invalid}"
aria-required="${this.required}"
aria-disabled="${this.disabled}"
>
<slot name="trigger">
<span>${unsafeHTML(this.displayValue)}</span>
</slot>
Expand Down Expand Up @@ -319,7 +327,7 @@ export class GdsDropdown<ValueT = any>
// Wrapped in a slot for backwards compatibility with the deprecated message slot
// Remove for 2.0 release
() => html`
<slot name="message" slot="message">
<slot id="message" name="message" slot="message">
<gds-icon-triangle-exclamation
solid
></gds-icon-triangle-exclamation>
Expand Down
1 change: 0 additions & 1 deletion libs/core/src/primitives/field-base/field-base.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export const styles = css`
@layer base {
* {
box-sizing: border-box;
user-select: none;
}
.field {
Expand Down

0 comments on commit 93bac39

Please sign in to comment.