Skip to content

Commit

Permalink
fix(skip-link): allow focusing without url navigation (#704)
Browse files Browse the repository at this point in the history
  • Loading branch information
samrichardsontylertech authored Oct 22, 2024
1 parent 3b59872 commit f512ebb
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 23 deletions.
3 changes: 2 additions & 1 deletion src/dev/pages/skip-link/skip-link.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
{ label: 'Info', value: 'info' }
]
},
{ type: 'switch', label: 'Muted', id: 'opt-muted' }
{ type: 'switch', label: 'Muted', id: 'opt-muted' },
{ type: 'switch', label: 'Skip URL change', id: 'opt-skip-url-change' }
]
}
})
Expand Down
5 changes: 5 additions & 0 deletions src/dev/pages/skip-link/skip-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const skipLink = document.getElementById('skip-link') as SkipLinkComponent;
const persistentSwitch = document.getElementById('opt-persistent') as SwitchComponent;
const themeSelect = document.getElementById('opt-theme') as SelectComponent;
const mutedSwitch = document.getElementById('opt-muted') as SwitchComponent;
const skipUrlChangeSwitch = document.getElementById('opt-skip-url-change') as SwitchComponent;

persistentSwitch.addEventListener('forge-switch-change', ({ detail }) => {
skipLink.persistent = detail;
Expand All @@ -23,3 +24,7 @@ themeSelect.addEventListener('change', ({ detail }) => {
mutedSwitch.addEventListener('forge-switch-change', ({ detail }) => {
skipLink.muted = detail;
});

skipUrlChangeSwitch.addEventListener('forge-switch-change', ({ detail }) => {
skipLink.skipUrlChange = detail;
});
3 changes: 2 additions & 1 deletion src/lib/skip-link/skip-link-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const observedAttributes = {
THEME: 'theme',
MUTED: 'muted',
PERSISTENT: 'persistent',
INLINE: 'inline'
INLINE: 'inline',
SKIP_URL_CHANGE: 'skip-url-change'
};

const attributes = {
Expand Down
28 changes: 28 additions & 0 deletions src/lib/skip-link/skip-link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ describe('SkipLink', () => {
expect(el.hasAttribute(SKIP_LINK_CONSTANTS.attributes.INLINE)).to.be.true;
});

it('should update the skip-url-change attribute when the property is set', async () => {
const el = await fixture<ISkipLinkComponent>(html`<forge-skip-link></forge-skip-link>`);
el.skipUrlChange = true;
expect(el.hasAttribute(SKIP_LINK_CONSTANTS.attributes.SKIP_URL_CHANGE)).to.be.true;
});

it('should change the target property when set via attribute', async () => {
const el = await fixture<ISkipLinkComponent>(html`<forge-skip-link></forge-skip-link>`);
el.setAttribute(SKIP_LINK_CONSTANTS.attributes.TARGET, 'main');
Expand Down Expand Up @@ -86,6 +92,28 @@ describe('SkipLink', () => {
expect(el.inline).to.be.true;
});

it('should change the skipUrlChange property when set via attribute', async () => {
const el = await fixture<ISkipLinkComponent>(html`<forge-skip-link></forge-skip-link>`);
el.setAttribute(SKIP_LINK_CONSTANTS.attributes.SKIP_URL_CHANGE, '');
expect(el.skipUrlChange).to.be.true;
});

it('should focus the target element when clicked and skipUrlChange is true', async () => {
const el = await fixture<ISkipLinkComponent>(html`
<div>
<forge-skip-link target="main" skip-url-change></forge-skip-link>
<div id="main" tabindex="-1"></div>
</div>
`);
const skipLink = el.querySelector('forge-skip-link');
const target = el.querySelector('#main');
const anchor = getAnchorEl(skipLink!);

anchor.click();

expect(document.activeElement).to.equal(target);
});

function getAnchorEl(el: ISkipLinkComponent): HTMLAnchorElement {
return el.shadowRoot!.querySelector(SKIP_LINK_CONSTANTS.selectors.ANCHOR) as HTMLAnchorElement;
}
Expand Down
73 changes: 61 additions & 12 deletions src/lib/skip-link/skip-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ISkipLinkComponent extends IBaseComponent {
muted: boolean;
persistent: boolean;
inline: boolean;
skipUrlChange: boolean;
}

declare global {
Expand All @@ -26,18 +27,6 @@ declare global {
*
* @summary The Forge Skip Link component is used to provide a way for users to skip repetitive content and navigate directly to a section of the page.
*
* @property {string} [target=''] - The IDREF of the element to which the skip link should navigate.
* @property {SkipLinkTheme} [theme='default'] - The theme applied to the skip link.
* @property {boolean} [muted=false] - Whether or not the skip link uses a muted color scheme.
* @property {boolean} [persistent=false] - Whether or not the skip link should remain visible when not focused.
* @property {boolean} [inline=false] - Whether or not the skip link renders within its container.
*
* @attribute {string} [target=''] - The IDREF of the element to which the skip link should navigate.
* @attribute {SkipLinkTheme} [theme='default'] - The theme applied to the skip link.
* @attribute {boolean} [muted=false] - Whether or not the skip link uses a muted color scheme.
* @attribute {boolean} [persistent=false] - Whether or not the skip link should remain visible when not focused.
* @attribute {boolean} [inline=false] - Whether or not the skip link renders within its container.
*
* @cssproperty --forge-skip-link-background - The background color of the skip link.
* @cssproperty --forge-skip-link-color - The text color of the skip link.
* @cssproperty --forge-skip-link-shape - The border radius of the skip link.
Expand Down Expand Up @@ -73,8 +62,11 @@ export class SkipLinkComponent extends BaseComponent implements ISkipLinkCompone
private _muted = false;
private _persistent = false;
private _inline = false;
private _skipUrlChange = false;
private _anchorElement: HTMLAnchorElement;

private _clickListener: EventListener = (evt: Event) => this._handleClick(evt);

constructor() {
super();
attachShadowTemplate(this, template, style);
Expand All @@ -98,9 +90,17 @@ export class SkipLinkComponent extends BaseComponent implements ISkipLinkCompone
case SKIP_LINK_CONSTANTS.observedAttributes.INLINE:
this.inline = coerceBoolean(newValue);
break;
case SKIP_LINK_CONSTANTS.observedAttributes.SKIP_URL_CHANGE:
this.skipUrlChange = coerceBoolean(newValue);
break;
}
}

/**
* The IDREF of the element to which the skip link should navigate.
* @default ''
* @attribute
*/
public get target(): string {
return this._target;
}
Expand All @@ -113,6 +113,11 @@ export class SkipLinkComponent extends BaseComponent implements ISkipLinkCompone
}
}

/**
* The theme applied to the skip link.
* @default 'default'
* @attribute
*/
public get theme(): SkipLinkTheme {
return this._theme;
}
Expand All @@ -123,6 +128,11 @@ export class SkipLinkComponent extends BaseComponent implements ISkipLinkCompone
}
}

/**
* Whether or not the skip link uses a muted color scheme.
* @default false
* @attribute
*/
public get muted(): boolean {
return this._muted;
}
Expand All @@ -133,6 +143,11 @@ export class SkipLinkComponent extends BaseComponent implements ISkipLinkCompone
}
}

/**
* Whether or not the skip link should remain visible when not focused.
* @default false
* @attribute
*/
public get persistent(): boolean {
return this._persistent;
}
Expand All @@ -143,6 +158,11 @@ export class SkipLinkComponent extends BaseComponent implements ISkipLinkCompone
}
}

/**
* Whether or not the skip link renders within its container.
* @default false
* @attribute
*/
public get inline(): boolean {
return this._inline;
}
Expand All @@ -152,4 +172,33 @@ export class SkipLinkComponent extends BaseComponent implements ISkipLinkCompone
this.toggleAttribute(SKIP_LINK_CONSTANTS.attributes.INLINE, this._inline);
}
}

/**
* Sets the skip link to skip browser navigation and scroll to the target element.
* @default false
* @attribute
*/
public get skipUrlChange(): boolean {
return this._skipUrlChange;
}
public set skipUrlChange(value: boolean) {
if (this._skipUrlChange !== value) {
this._skipUrlChange = value;
this.toggleAttribute(SKIP_LINK_CONSTANTS.attributes.SKIP_URL_CHANGE, this._skipUrlChange);

if (this._skipUrlChange) {
this._anchorElement.addEventListener('click', this._clickListener);
return;
}

this._anchorElement.removeEventListener('click', this._clickListener);
}
}

private _handleClick(evt: Event): void {
evt.preventDefault();
const targetElement = document.getElementById(this._target);
targetElement?.focus();
targetElement?.scrollIntoView({ behavior: 'smooth' });
}
}
19 changes: 10 additions & 9 deletions src/stories/components/skip-link/SkipLink.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ const meta = {
const cssVarArgs = getCssVariableArgs(args);
const style = cssVarArgs ? styleMap(cssVarArgs) : nothing;
return html`
<forge-skip-link
.target=${args.target}
.theme=${args.theme}
.muted=${args.muted}
.persistent=${args.persistent}
.inline=${args.inline}
style=${style}></forge-skip-link>
<div>
<forge-skip-link
target="main-content"
.theme=${args.theme}
.muted=${args.muted}
.persistent=${args.persistent}
.inline=${args.inline}
style=${style}></forge-skip-link>
<div id="main-content" tabindex="0">Target</div>
</div>
`;
},
parameters: {
Expand All @@ -30,7 +33,6 @@ const meta = {
...generateCustomElementArgTypes({
tagName: component,
controls: {
target: { control: 'text' },
theme: { control: 'select', options: ['default', 'primary', 'secondary', 'tertiary', 'success', 'error', 'warning', 'info'] },
muted: { control: 'boolean' },
persistent: { control: 'boolean' },
Expand All @@ -39,7 +41,6 @@ const meta = {
})
},
args: {
target: 'main-content',
theme: 'default',
muted: false,
persistent: false,
Expand Down

0 comments on commit f512ebb

Please sign in to comment.