Skip to content
Open
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
4 changes: 4 additions & 0 deletions shepherd.js/src/components/shepherd-button.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script>
import { isFunction } from '../utils/type-check.ts';
import { convertDataAttributes } from '../utils/data-attributes.ts';

let { config, step } = $props();

Expand All @@ -18,6 +19,8 @@
const label = $derived(config.label ? getConfigOption(config.label) : null);
const secondary = $derived(config.secondary);
const text = $derived(config.text ? getConfigOption(config.text) : null);

const dataAttrs = $derived(convertDataAttributes(config.dataAttributes));
</script>

<button
Expand All @@ -29,6 +32,7 @@
onclick={action}
tabindex="0"
type="button"
{...dataAttrs}
>
{@html text}
</button>
Expand Down
5 changes: 5 additions & 0 deletions shepherd.js/src/components/shepherd-cancel-icon.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script>
import { convertDataAttributes } from '../utils/data-attributes.ts';
let { cancelIcon, step } = $props();
/**
Expand All @@ -8,13 +10,16 @@
e.preventDefault();
step.cancel();
};
const dataAttrs = $derived(convertDataAttributes(cancelIcon.dataAttributes));
</script>

<button
aria-label={cancelIcon.label ? cancelIcon.label : 'Close Tour'}
class="shepherd-cancel-icon"
onclick={handleCancelClick}
type="button"
{...dataAttrs}
>
<span aria-hidden="true">&times;</span>
</button>
Expand Down
41 changes: 41 additions & 0 deletions shepherd.js/src/utils/data-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Represents a single data attribute with an id and value
*/
export interface DataAttribute {
id: string;
value: string | number | boolean;
}

/**
* Converts an array of data attributes to an object of data-* attributes
* suitable for spreading onto HTML elements.
*
* @param dataAttributes - Array of data attribute objects with id and value
* @returns Object with data-* attributes as keys
*
* @example
* ```typescript
* const attrs = convertDataAttributes([
* { id: 'foo', value: 'bar' },
* { id: 'count', value: 42 }
* ]);
* // Returns: { 'data-foo': 'bar', 'data-count': '42' }
* ```
*/
export function convertDataAttributes(
dataAttributes?: DataAttribute[] | null
): Record<string, string> {
if (!dataAttributes || !Array.isArray(dataAttributes)) {
return {};
}

return dataAttributes.reduce(
(acc, attr) => {
if (attr.id) {
acc[`data-${attr.id}`] = String(attr.value);
}
return acc;
},
{} as Record<string, string>
);
}
146 changes: 146 additions & 0 deletions test/unit/components/shepherd-button.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,150 @@ describe('component/ShepherdButton', () => {
expect(buttonUpdated).toHaveTextContent('Test 2');
});
});

describe('dataAttributes', () => {
it('applies single data attribute correctly', () => {
const config = {
text: 'Click me',
dataAttributes: [{ id: 'test', value: 'testValue' }]
};

const { container } = render(ShepherdButton, {
props: {
config
}
});

const button = container.querySelector('.shepherd-button');
expect(button).toHaveAttribute('data-test', 'testValue');
});

it('applies multiple data attributes correctly', () => {
const config = {
text: 'Click me',
dataAttributes: [
{ id: 'foo', value: 'someData' },
{ id: 'bar', value: '1234' },
{ id: 'baz', value: 'anotherValue' }
]
};

const { container } = render(ShepherdButton, {
props: {
config
}
});

const button = container.querySelector('.shepherd-button');
expect(button).toHaveAttribute('data-foo', 'someData');
expect(button).toHaveAttribute('data-bar', '1234');
expect(button).toHaveAttribute('data-baz', 'anotherValue');
});

it('handles data attributes with numeric values', () => {
const config = {
text: 'Click me',
dataAttributes: [
{ id: 'count', value: 42 },
{ id: 'price', value: 99.99 }
]
};

const { container } = render(ShepherdButton, {
props: {
config
}
});

const button = container.querySelector('.shepherd-button');
expect(button).toHaveAttribute('data-count', '42');
expect(button).toHaveAttribute('data-price', '99.99');
});

it('handles empty dataAttributes array', () => {
const config = {
text: 'Click me',
dataAttributes: []
};

const { container } = render(ShepherdButton, {
props: {
config
}
});

const button = container.querySelector('.shepherd-button');
const dataAttrs = Array.from(button.attributes).filter((attr) =>
attr.name.startsWith('data-')
);
expect(dataAttrs).toHaveLength(0);
});

it('handles undefined dataAttributes', () => {
const config = {
text: 'Click me'
};

const { container } = render(ShepherdButton, {
props: {
config
}
});

const button = container.querySelector('.shepherd-button');
expect(button).toBeInTheDocument();
});

it('ignores data attributes without id', () => {
const config = {
text: 'Click me',
dataAttributes: [
{ id: 'valid', value: 'validValue' },
{ value: 'noId' },
{ id: '', value: 'emptyId' }
]
};

const { container } = render(ShepherdButton, {
props: {
config
}
});

const button = container.querySelector('.shepherd-button');
expect(button).toHaveAttribute('data-valid', 'validValue');

const dataAttrs = Array.from(button.attributes).filter((attr) =>
attr.name.startsWith('data-')
);
expect(dataAttrs).toHaveLength(1);
});

it('works with other button properties', () => {
const config = {
text: 'Next',
label: 'Go to next step',
classes: 'custom-class',
secondary: true,
dataAttributes: [
{ id: 'step', value: '2' },
{ id: 'action', value: 'next' }
]
};

const { container } = render(ShepherdButton, {
props: {
config
}
});

const button = container.querySelector('.shepherd-button');
expect(button).toHaveAttribute('aria-label', 'Go to next step');
expect(button).toHaveClass('custom-class');
expect(button).toHaveClass('shepherd-button-secondary');
expect(button).toHaveAttribute('data-step', '2');
expect(button).toHaveAttribute('data-action', 'next');
expect(button).toHaveTextContent('Next');
});
});
});
Loading