Skip to content

Commit

Permalink
fix(ssr): ensure UID consistency between server and client
Browse files Browse the repository at this point in the history
closes #160
  • Loading branch information
LeBenLeBen committed Mar 2, 2022
1 parent 636fbbe commit f554543
Show file tree
Hide file tree
Showing 25 changed files with 220 additions and 72 deletions.
17 changes: 17 additions & 0 deletions packages/chusho/cypress/integration/CSelect.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
describe('Select', () => {
describe('default', () => {
beforeEach(() => {
cy.visitComponent('select/default');
});

it('links SelectBtn with SelectOptions', () => {
cy.get('[data-test="select-button"]').trigger('click');
cy.get('[data-test="select-button"]').then(($el) => {
cy.wrap($el).should(
'have.attr',
'aria-controls',
Cypress.$(`[data-test="select-options"]`).attr('id')
);
});
});
});

describe('With Validation', () => {
beforeEach(() => {
cy.visitComponent('select/with-validation');
Expand Down
11 changes: 11 additions & 0 deletions packages/chusho/cypress/integration/CTabs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ describe('Tabs', () => {
.trigger('keydown', { key: 'End' });
cy.get('[data-test="tab-3"]').should('have.focus');
});

it('links Tab with TabPanel', () => {
cy.get('[data-test="tab-2"]').trigger('click');
cy.get('[data-test="tab-2"]').then(($el) => {
cy.wrap($el).should(
'have.attr',
'aria-controls',
Cypress.$(`[data-test="tabpanel-2"]`).attr('id')
);
});
});
});

describe('controlled', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/chusho/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = {
preset: 'ts-jest/presets/js-with-babel',
testMatch: ['<rootDir>lib/**/?(*.)+(spec).[jt]s?(x)'],
moduleFileExtensions: ['ts', 'js', 'd.ts', 'json', 'jsx', 'tsx', 'node'],
setupFilesAfterEnv: ['<rootDir>config/jest/setup.js'],
setupFilesAfterEnv: ['<rootDir>/jest/setup.js'],
globals: {
'ts-jest': {
diagnostics: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import uid, { reset } from '../../lib/utils/uid';
import uid, { reset } from '../lib/utils/uid';

/*----------------------------------------*\
Spy on console.warn to assert on it
Expand Down Expand Up @@ -62,7 +62,7 @@ afterEach(() => {
Keep the same UID sequence for each test
\*----------------------------------------*/

jest.mock('../../lib/utils/uid');
jest.mock('../lib/utils/uid');

afterEach(() => {
uid.mockClear();
Expand Down
19 changes: 19 additions & 0 deletions packages/chusho/jest/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { mount } from '@vue/test-utils';

/**
* Wrap a composable into a component setup
* Required for composables using lifecycle hooks
*/
export function withSetup(composable) {
let result;
mount({
setup() {
result = composable();
// Suppress missing template warning
return () => {
// noop
};
},
});
return result;
}
10 changes: 4 additions & 6 deletions packages/chusho/lib/components/CCollapse/CCollapse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import useComponentConfig from '../../composables/useComponentConfig';
import useTogglable from '../../composables/useTogglable';

import { generateConfigClass } from '../../utils/components';
import uid from '../../utils/uid';

export const CollapseSymbol: InjectionKey<UseCollapse> = Symbol('CCollapse');
export const CollapseSymbol: InjectionKey<Collapse> = Symbol('CCollapse');

export interface UseCollapse {
uid: string;
export interface Collapse {
toggle: ReturnType<typeof useTogglable>;
}

Expand All @@ -35,8 +33,7 @@ export default defineComponent({
emits: ['update:modelValue'],

setup(props) {
const collapse: UseCollapse = {
uid: uid('chusho-collapse'),
const collapse: Collapse = {
toggle: useTogglable(props.modelValue),
};

Expand All @@ -59,6 +56,7 @@ export default defineComponent({
...this.$props,
active: isActive,
}),
...this.collapse?.toggle.uid.cacheAttrs,
};

return h(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('CCollapseBtn', () => {
});

expect(wrapper.findComponent(CCollapseBtn).html()).toBe(
'<button aria-expanded="false" aria-controls="chusho-toggle-1" type="button"></button>'
'<button aria-expanded="false" aria-controls="chusho-toggle-0" type="button"></button>'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('CCollapseContent', () => {
});

expect(wrapper.findComponent(CCollapseContent).html()).toBe(
'<div id="chusho-toggle-1"></div>'
'<div id="chusho-toggle-0"></div>'
);
});

Expand Down Expand Up @@ -75,7 +75,7 @@ describe('CCollapseContent', () => {
},
});

expect(wrapper.find('#chusho-toggle-1').classes()).toEqual([
expect(wrapper.find('#chusho-toggle-0').classes()).toEqual([
'fade-enter-from',
'fade-enter-active',
]);
Expand Down
11 changes: 6 additions & 5 deletions packages/chusho/lib/components/CIcon/CIcon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { defineComponent, h, mergeProps } from 'vue';

import componentMixin from '../mixins/componentMixin';

import useCachedUid from '../../composables/useCachedUid';
import useComponentConfig from '../../composables/useComponentConfig';

import { generateConfigClass } from '../../utils/components';
import uid from '../../utils/uid';

export default defineComponent({
name: 'CIcon',
Expand Down Expand Up @@ -48,6 +48,7 @@ export default defineComponent({
setup() {
return {
config: useComponentConfig('icon'),
uid: useCachedUid('chusho-icon'),
};
},

Expand All @@ -57,8 +58,8 @@ export default defineComponent({
width: `${(this.config?.width || 24) * this.scale}`,
height: `${(this.config?.height || 24) * this.scale}`,
...generateConfigClass(this.config?.class, this.$props),
...this.uid.cacheAttrs,
});
const id = uid('chusho-icon');

const children = [
h('use', {
Expand All @@ -68,14 +69,14 @@ export default defineComponent({
];

if (this.alt) {
elementProps['aria-labelledby'] = id;
elementProps['aria-labelledby'] = this.uid.id.value;
children.unshift(
h(
'title',
{
id,
id: this.uid.id.value,
},
[this.alt]
this.alt
)
);
} else {
Expand Down
4 changes: 1 addition & 3 deletions packages/chusho/lib/components/CSelect/CSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import useTogglable from '../../composables/useTogglable';

import { ALL_TYPES, generateConfigClass } from '../../utils/components';
import { isObject, isPrimitive } from '../../utils/objects';
import uid from '../../utils/uid';

export const SelectSymbol: InjectionKey<Select> = Symbol('CSelect');

Expand All @@ -33,7 +32,6 @@ export interface SelectOptionData {
export type SelectOption = SelectedItem<SelectOptionData>;

export interface Select {
uid: string;
value: ComputedRef<SelectValue>;
setValue: (value: SelectValue) => void;
disabled: ComputedRef<boolean>;
Expand Down Expand Up @@ -107,7 +105,6 @@ export default defineComponent({

setup(props, { emit }) {
const select: Select = {
uid: uid('chusho-select'),
value: computed(() => props.modelValue),
setValue: (value: unknown) => {
emit('update:modelValue', value);
Expand Down Expand Up @@ -140,6 +137,7 @@ export default defineComponent({
render() {
const elementProps: Record<string, unknown> = {
...generateConfigClass(this.config?.class, this.$props),
...this.select.togglable.uid.cacheAttrs,
onKeydown: this.handleKeydown,
};
const inputProps: Record<string, unknown> = {
Expand Down
4 changes: 2 additions & 2 deletions packages/chusho/lib/components/CSelect/CSelectBtn.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('CSelectBtn', () => {
});

expect(wrapper.findComponent(CSelectBtn).html()).toBe(
'<button aria-expanded="false" aria-controls="chusho-toggle-1" aria-haspopup="listbox" type="button">Label</button>'
'<button aria-expanded="false" aria-controls="chusho-toggle-0" aria-haspopup="listbox" type="button">Label</button>'
);
});

Expand All @@ -28,7 +28,7 @@ describe('CSelectBtn', () => {
});

expect(wrapper.findComponent(CSelectBtn).html()).toBe(
'<button aria-expanded="true" aria-controls="chusho-toggle-1" aria-haspopup="listbox" type="button">Label</button>'
'<button aria-expanded="true" aria-controls="chusho-toggle-0" aria-haspopup="listbox" type="button">Label</button>'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('CSelectGroup', () => {
});

expect(wrapper.findComponent(CSelectGroup).html()).toBe(
'<div role="group" aria-labelledby="chusho-select-group-label-2">Slot</div>'
'<div role="group" aria-labelledby="chusho-select-group-label-1">Slot</div>'
);
});

Expand Down
17 changes: 9 additions & 8 deletions packages/chusho/lib/components/CSelect/CSelectGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { InjectionKey, defineComponent, h, mergeProps, provide } from 'vue';

import componentMixin from '../mixins/componentMixin';

import useCachedUid, { UseCachedUid } from '../../composables/useCachedUid';
import useComponentConfig from '../../composables/useComponentConfig';

import { generateConfigClass } from '../../utils/components';
import uid from '../../utils/uid';

export const SelectGroupSymbol: InjectionKey<UseSelectGroup> =
export const SelectGroupSymbol: InjectionKey<SelectGroup> =
Symbol('CSelectGroup');

export interface UseSelectGroup {
labelId: string;
export interface SelectGroup {
labelUid: UseCachedUid;
}

export default defineComponent({
Expand All @@ -22,8 +22,8 @@ export default defineComponent({
inheritAttrs: false,

setup() {
const selectGroup: UseSelectGroup = {
labelId: uid('chusho-select-group-label'),
const selectGroup: SelectGroup = {
labelUid: useCachedUid('chusho-select-group-label'),
};

provide(SelectGroupSymbol, selectGroup);
Expand All @@ -36,9 +36,10 @@ export default defineComponent({

render() {
const elementProps: Record<string, unknown> = {
role: 'group',
'aria-labelledby': this.selectGroup.labelId,
...generateConfigClass(this.config?.class, this.$props),
...this.selectGroup.labelUid.cacheAttrs,
role: 'group',
'aria-labelledby': this.selectGroup.labelUid.id.value,
};

return h('div', mergeProps(this.$attrs, elementProps), this.$slots);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export default defineComponent({

render() {
const elementProps: Record<string, unknown> = {
id: this.selectGroup?.labelId,
...generateConfigClass(this.config?.class, this.$props),
id: this.selectGroup?.labelUid.id.value,
};

return h('div', mergeProps(this.$attrs, elementProps), this.$slots);
Expand Down
10 changes: 5 additions & 5 deletions packages/chusho/lib/components/CSelect/CSelectOption.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('CSelectOption', () => {
});

expect(wrapper.findComponent(CSelectOption).html()).toBe(
'<li id="chusho-select-option-2" role="option" tabindex="-1" aria-selected="true">Label</li>'
'<li id="chusho-select-option-1" role="option" tabindex="-1" aria-selected="true">Label</li>'
);
});

Expand All @@ -29,7 +29,7 @@ describe('CSelectOption', () => {
});

expect(wrapper.findComponent(CSelectOption).html()).toBe(
'<li id="chusho-select-option-2" role="option" tabindex="-1">Label</li>'
'<li id="chusho-select-option-1" role="option" tabindex="-1">Label</li>'
);
});

Expand All @@ -45,7 +45,7 @@ describe('CSelectOption', () => {
});

expect(wrapper.findComponent(CSelectOption).html()).toBe(
'<li id="chusho-select-option-2" role="option" tabindex="-1" aria-disabled="true">Label</li>'
'<li id="chusho-select-option-1" role="option" tabindex="-1" aria-disabled="true">Label</li>'
);
});

Expand Down Expand Up @@ -95,7 +95,7 @@ describe('CSelectOption', () => {
expect(wrapper.vm.select.selectable.items.value).toEqual([
{
data: { disabled: false, text: 'label' },
id: 'chusho-select-option-2',
id: 'chusho-select-option-1',
},
]);
});
Expand Down Expand Up @@ -124,7 +124,7 @@ describe('CSelectOption', () => {

expect(wrapper.vm.select.selectable.selectedItem.value).toEqual({
data: { disabled: false, text: 'label' },
id: 'chusho-select-option-2',
id: 'chusho-select-option-1',
});
});

Expand Down
3 changes: 2 additions & 1 deletion packages/chusho/lib/components/CSelect/CSelectOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import componentMixin from '../mixins/componentMixin';
import useComponentConfig from '../../composables/useComponentConfig';

import { ALL_TYPES, generateConfigClass } from '../../utils/components';
import { isClient } from '../../utils/ssr';
import uid from '../../utils/uid';

import { SelectSymbol } from './CSelect';
Expand Down Expand Up @@ -106,7 +107,7 @@ export default defineComponent({
}),
};

if (this.isFocused) {
if (isClient && this.isFocused) {
nextTick(() => {
this.$el.focus();
});
Expand Down
4 changes: 2 additions & 2 deletions packages/chusho/lib/components/CSelect/CSelectOptions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ describe('CSelectOption', () => {
await nextTick();

expect(wrapper.findComponent(CSelectOptions).attributes()).toEqual({
id: 'chusho-toggle-1',
id: 'chusho-toggle-0',
role: 'listbox',
'aria-activedescendant': 'chusho-select-option-2',
'aria-activedescendant': 'chusho-select-option-1',
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/chusho/lib/components/CTabs/CTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export default defineComponent({
const isActive = this.target === this.tabs.selectedItemId.value;
const elementProps = {
type: 'button',
id: `${this.tabs.uid}-tab-${this.target}`,
id: `${this.tabs.uid.id.value}-tab-${this.target}`,
role: 'tab',
'aria-selected': `${isActive}`,
'aria-controls': `${this.tabs.uid}-tabpanel-${this.target}`,
'aria-controls': `${this.tabs.uid.id.value}-tabpanel-${this.target}`,
tabindex: isActive ? '0' : '-1',
onClick: () => {
if (!['string', 'number'].includes(typeof this.target)) return;
Expand Down
Loading

0 comments on commit f554543

Please sign in to comment.