Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
38 changes: 38 additions & 0 deletions shepherd.js/src/utils/data-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* 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
Loading