diff --git a/packages/angular/src/lib/stencil-generated/components.ts b/packages/angular/src/lib/stencil-generated/components.ts index 9448baf55..7708d4010 100644 --- a/packages/angular/src/lib/stencil-generated/components.ts +++ b/packages/angular/src/lib/stencil-generated/components.ts @@ -1096,14 +1096,14 @@ export declare interface GcdsSrOnly extends Components.GcdsSrOnly {} @ProxyCmp({ - inputs: ['currentStep', 'totalSteps'] + inputs: ['currentStep', 'tag', 'totalSteps'] }) @Component({ selector: 'gcds-stepper', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['currentStep', 'totalSteps'], + inputs: ['currentStep', 'tag', 'totalSteps'], }) export class GcdsStepper { protected el: HTMLElement; diff --git a/packages/vue/lib/components.ts b/packages/vue/lib/components.ts index 873d351a9..1a779eac9 100644 --- a/packages/vue/lib/components.ts +++ b/packages/vue/lib/components.ts @@ -404,7 +404,8 @@ export const GcdsSrOnly = /*@__PURE__*/ defineContainer('gcds-sr export const GcdsStepper = /*@__PURE__*/ defineContainer('gcds-stepper', undefined, [ 'currentStep', - 'totalSteps' + 'totalSteps', + 'tag' ]); diff --git a/packages/web/src/components.d.ts b/packages/web/src/components.d.ts index 121c5e29a..fef516a03 100644 --- a/packages/web/src/components.d.ts +++ b/packages/web/src/components.d.ts @@ -1017,6 +1017,10 @@ export namespace Components { * Defines the current step. */ "currentStep": number; + /** + * Defines the heading tag to render + */ + "tag": 'h1' | 'h2' | 'h3'; /** * Defines the total amount of steps. */ @@ -3014,6 +3018,10 @@ declare namespace LocalJSX { * Defines the current step. */ "currentStep": number; + /** + * Defines the heading tag to render + */ + "tag"?: 'h1' | 'h2' | 'h3'; /** * Defines the total amount of steps. */ diff --git a/packages/web/src/components/gcds-sr-only/readme.md b/packages/web/src/components/gcds-sr-only/readme.md index 86aa46fe9..27b487aa4 100644 --- a/packages/web/src/components/gcds-sr-only/readme.md +++ b/packages/web/src/components/gcds-sr-only/readme.md @@ -21,6 +21,7 @@ - [gcds-footer](../gcds-footer) - [gcds-lang-toggle](../gcds-lang-toggle) - [gcds-search](../gcds-search) + - [gcds-stepper](../gcds-stepper) - [gcds-topic-menu](../gcds-topic-menu) ### Graph @@ -31,6 +32,7 @@ graph TD; gcds-footer --> gcds-sr-only gcds-lang-toggle --> gcds-sr-only gcds-search --> gcds-sr-only + gcds-stepper --> gcds-sr-only gcds-topic-menu --> gcds-sr-only style gcds-sr-only fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/web/src/components/gcds-stepper/gcds-stepper.css b/packages/web/src/components/gcds-stepper/gcds-stepper.css index aafc559b9..e398ff3f1 100644 --- a/packages/web/src/components/gcds-stepper/gcds-stepper.css +++ b/packages/web/src/components/gcds-stepper/gcds-stepper.css @@ -7,7 +7,14 @@ } @layer default { - :host .gcds-stepper { - color: var(--gcds-stepper-text); + :host .gcds-stepper .gcds-stepper__steps { + font: var(--gcds-stepper-font-desktop); + display: block; + margin: var(--gcds-stepper-margin-desktop); + + @media only screen and (width < 48em) { + font: var(--gcds-stepper-font-mobile); + margin: var(--gcds-stepper-margin-mobile); + } } } diff --git a/packages/web/src/components/gcds-stepper/gcds-stepper.tsx b/packages/web/src/components/gcds-stepper/gcds-stepper.tsx index 41d9da066..2459f7de2 100644 --- a/packages/web/src/components/gcds-stepper/gcds-stepper.tsx +++ b/packages/web/src/components/gcds-stepper/gcds-stepper.tsx @@ -1,5 +1,5 @@ -import { Component, Element, Host, Prop, State, h } from '@stencil/core'; -import { assignLanguage, observerConfig } from '../../utils/utils'; +import { Component, Element, Host, Prop, State, Watch, h } from '@stencil/core'; +import { assignLanguage, observerConfig, logError } from '../../utils/utils'; import i18n from './i18n/i18n'; @Component({ @@ -17,12 +17,47 @@ export class GcdsStepper { /** * Defines the current step. */ - @Prop() currentStep!: number; + @Prop({ mutable: true }) currentStep!: number; + @Watch('currentStep') + validateCurrentStep() { + if ( + this.currentStep <= 0 || + isNaN(this.currentStep) || + this.currentStep > this.totalSteps + ) { + this.errors.push('currentStep'); + } else if (this.errors.includes('currentStep')) { + this.errors.splice(this.errors.indexOf('currentStep'), 1); + } + } /** * Defines the total amount of steps. */ - @Prop() totalSteps!: number; + @Prop({ mutable: true }) totalSteps!: number; + @Watch('totalSteps') + validateTotalSteps() { + if ( + this.totalSteps <= 0 || + isNaN(this.totalSteps) || + this.totalSteps < this.currentStep + ) { + this.errors.push('totalSteps'); + } else if (this.errors.includes('totalSteps')) { + this.errors.splice(this.errors.indexOf('totalSteps'), 1); + } + } + + /** + * Defines the heading tag to render + */ + @Prop() tag: 'h1' | 'h2' | 'h3' = 'h2'; + + /** + * State to track validation on properties + * Contains a list of properties that have an error associated with them + */ + @State() errors: Array = []; /** * Language of rendered component @@ -41,26 +76,63 @@ export class GcdsStepper { observer.observe(this.el, observerConfig); } + private validateChildren() { + if (this.el.innerHTML.trim() == '') { + this.errors.push('children'); + } else if (this.errors.includes('children')) { + this.errors.splice(this.errors.indexOf('children'), 1); + } + } + + private validateRequiredProps() { + this.validateCurrentStep(); + this.validateTotalSteps(); + this.validateChildren(); + + if ( + this.errors.includes('totalSteps') || + this.errors.includes('currentStep') || + this.errors.includes('children') + ) { + return false; + } + return true; + } + async componentWillLoad() { // Define lang attribute this.lang = assignLanguage(this.el); this.updateLang(); + + let valid = this.validateRequiredProps(); + + if (!valid) { + logError('gcds-stepper', this.errors); + } } render() { - const { currentStep, lang, totalSteps } = this; + const { currentStep, lang, totalSteps, tag } = this; return ( - - {`${i18n[lang].step} ${currentStep} ${i18n[lang].of} ${totalSteps}`} - + {this.validateRequiredProps() && ( + + + {`${i18n[lang].step} ${currentStep} ${i18n[lang].of} ${totalSteps}`} + + {/* Hidden colon to provide pause between caption and heading text for AT */} + : + + + + )} ); } diff --git a/packages/web/src/components/gcds-stepper/readme.md b/packages/web/src/components/gcds-stepper/readme.md index cc100ba39..9d8b7bfa0 100644 --- a/packages/web/src/components/gcds-stepper/readme.md +++ b/packages/web/src/components/gcds-stepper/readme.md @@ -7,10 +7,11 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------------- | -------------- | ---------------------------------- | -------- | ----------- | -| `currentStep` _(required)_ | `current-step` | Defines the current step. | `number` | `undefined` | -| `totalSteps` _(required)_ | `total-steps` | Defines the total amount of steps. | `number` | `undefined` | +| Property | Attribute | Description | Type | Default | +| -------------------------- | -------------- | ---------------------------------- | ---------------------- | ----------- | +| `currentStep` _(required)_ | `current-step` | Defines the current step. | `number` | `undefined` | +| `tag` | `tag` | Defines the heading tag to render | `"h1" \| "h2" \| "h3"` | `'h2'` | +| `totalSteps` _(required)_ | `total-steps` | Defines the total amount of steps. | `number` | `undefined` | ## Dependencies @@ -18,11 +19,13 @@ ### Depends on - [gcds-heading](../gcds-heading) +- [gcds-sr-only](../gcds-sr-only) ### Graph ```mermaid graph TD; gcds-stepper --> gcds-heading + gcds-stepper --> gcds-sr-only style gcds-stepper fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/web/src/components/gcds-stepper/stories/gcds-stepper.stories.tsx b/packages/web/src/components/gcds-stepper/stories/gcds-stepper.stories.tsx index 9bffc711f..ac844ae79 100644 --- a/packages/web/src/components/gcds-stepper/stories/gcds-stepper.stories.tsx +++ b/packages/web/src/components/gcds-stepper/stories/gcds-stepper.stories.tsx @@ -27,6 +27,29 @@ export default { required: true, }, }, + tag: { + control: 'select', + options: ['h1', 'h2', 'h3'], + table: { + type: { summary: 'string' }, + defaultValue: { summary: 'h2' }, + }, + }, + + // Slots + default: { + control: { + type: 'text', + }, + description: + 'Customize the content or include additional elements. | Personnalisez le contenu ou ajoutez des éléments supplémentaires.', + table: { + category: 'Slots | Fentes', + }, + type: { + required: true, + }, + }, ...langProp, }, }; @@ -36,13 +59,15 @@ const Template = args => + }" tag="${args.tag}" ${args.lang != 'en' ? `lang="${args.lang}"` : null}> + ${args.default} + }" tag="${args.tag}" ${args.lang != 'en' ? `lang="${args.lang}"` : null}> + ${args.default} `.replace(/ null/g, ''); @@ -50,8 +75,9 @@ const TemplatePlayground = args => ` + tag="${args.tag}" + ${args.lang != 'en' ? `lang="${args.lang}"` : null}> + ${args.default} `; @@ -62,6 +88,41 @@ Default.args = { lang: 'en', currentStep: 1, totalSteps: 4, + tag: 'h2', + default: 'Section title', +}; + +// ------ Stepper tag: H1 ------ + +export const tagH1 = Template.bind({}); +tagH1.args = { + lang: 'en', + currentStep: 1, + totalSteps: 4, + tag: 'h1', + default: 'Section title', +}; + +// ------ Stepper tag: H2 ------ + +export const tagH2 = Template.bind({}); +tagH2.args = { + lang: 'en', + currentStep: 1, + totalSteps: 4, + tag: 'h2', + default: 'Section title', +}; + +// ------ Stepper tag: H3 ------ + +export const tagH3 = Template.bind({}); +tagH3.args = { + lang: 'en', + currentStep: 1, + totalSteps: 4, + tag: 'h3', + default: 'Section title', }; // ------ Stepper french ------ @@ -71,6 +132,8 @@ French.args = { lang: 'fr', currentStep: 1, totalSteps: 4, + tag: 'h2', + default: 'Section title', }; // ------ Stepper events & props ------ @@ -80,6 +143,8 @@ Props.args = { lang: 'en', currentStep: 1, totalSteps: 4, + tag: 'h2', + default: 'Section title', }; // ------ Stepper playground ------ @@ -89,4 +154,6 @@ Playground.args = { lang: 'en', currentStep: 1, totalSteps: 4, + tag: 'h2', + default: 'Section title', }; diff --git a/packages/web/src/components/gcds-stepper/stories/overview.mdx b/packages/web/src/components/gcds-stepper/stories/overview.mdx index 2f977ff89..76de427c2 100644 --- a/packages/web/src/components/gcds-stepper/stories/overview.mdx +++ b/packages/web/src/components/gcds-stepper/stories/overview.mdx @@ -15,6 +15,20 @@ A stepper is a progress tracker for a multi-step process.
+### Tag + +#### Heading 1 + + + +#### Heading 2 + + + +#### Heading 3 + + + ### Language #### English diff --git a/packages/web/src/components/gcds-stepper/test/gcds-stepper.e2e.ts b/packages/web/src/components/gcds-stepper/test/gcds-stepper.e2e.ts index 364464c7d..21608e312 100644 --- a/packages/web/src/components/gcds-stepper/test/gcds-stepper.e2e.ts +++ b/packages/web/src/components/gcds-stepper/test/gcds-stepper.e2e.ts @@ -4,7 +4,7 @@ const { AxePuppeteer } = require('@axe-core/puppeteer'); describe('gcds-stepper', () => { it('renders', async () => { const page = await newE2EPage(); - await page.setContent(''); + await page.setContent('Section title'); const element = await page.find('gcds-stepper'); expect(element).toHaveClass('hydrated'); @@ -23,11 +23,7 @@ describe('gcds-stepper a11y tests', () => { it('colour contrast', async () => { const page = await newE2EPage(); await page.setContent(` - - - + Section title `); const colorContrastTest = new AxePuppeteer(page) @@ -35,6 +31,22 @@ describe('gcds-stepper a11y tests', () => { .analyze(); const results = await colorContrastTest; + expect(results.violations.length).toBe(0); + }); + /** + * Discernable text in heading + */ + it('heading text', async () => { + const page = await newE2EPage(); + await page.setContent(` + Section title + `); + + const emptyHeadingtest = new AxePuppeteer(page) + .withRules('empty-heading') + .analyze(); + const results = await emptyHeadingtest; + expect(results.violations.length).toBe(0); }); }); diff --git a/packages/web/src/components/gcds-stepper/test/gcds-stepper.spec.tsx b/packages/web/src/components/gcds-stepper/test/gcds-stepper.spec.tsx index 8cfb5b7f3..04a278f65 100644 --- a/packages/web/src/components/gcds-stepper/test/gcds-stepper.spec.tsx +++ b/packages/web/src/components/gcds-stepper/test/gcds-stepper.spec.tsx @@ -5,47 +5,217 @@ describe('gcds-stepper', () => { it('renders', async () => { const page = await newSpecPage({ components: [GcdsStepper], - html: ``, + html: `Section title`, }); expect(page.root).toEqualHtml(` - Step 2 of 6 + + + Step 2 of 6 + : + + + Section title `); }); - /** - * Renders current + total steps - */ - it('renders steps', async () => { + it('renders - current and total steps', async () => { const page = await newSpecPage({ components: [GcdsStepper], - html: ``, + html: `Section title`, }); expect(page.root).toEqualHtml(` - Step 2 of 6 + + + Step 2 of 6 + : + + + + Section title + + `); + }); + + it('renders - tag h1', async () => { + const page = await newSpecPage({ + components: [GcdsStepper], + html: `Section title`, + }); + expect(page.root).toEqualHtml(` + + + + + Step 2 of 6 + : + + + + Section title + + `); + }); + + it('renders - tag h2', async () => { + const page = await newSpecPage({ + components: [GcdsStepper], + html: `Section title`, + }); + expect(page.root).toEqualHtml(` + + + + + Step 2 of 6 + : + + + + Section title + + `); + }); + + it('renders - tag h3', async () => { + const page = await newSpecPage({ + components: [GcdsStepper], + html: `Section title`, + }); + expect(page.root).toEqualHtml(` + + + + + Step 2 of 6 + : + + + Section title `); }); - /** - * Renders stepper in french - */ - it('renders stepper in french', async () => { + it('renders - French', async () => { const page = await newSpecPage({ components: [GcdsStepper], - html: ``, + html: `Section title`, }); expect(page.root).toEqualHtml(` - Étape 2 sur 6 + + + Étape 2 sur 6 + : + + + + Section title + + `); + }); + + it('renders - HTML children', async () => { + const page = await newSpecPage({ + components: [GcdsStepper], + html: ` + Section title + `, + }); + expect(page.root).toEqualHtml(` + + + + + Step 2 of 6 + : + + + + Section title + + `); + }); + + it('does not render - no children', async () => { + const page = await newSpecPage({ + components: [GcdsStepper], + html: ``, + }); + expect(page.root).toEqualHtml(` + + + + + `); + }); + + it('does not render - no required attributes', async () => { + const page = await newSpecPage({ + components: [GcdsStepper], + html: `Section title`, + }); + expect(page.root).toEqualHtml(` + + + + Section title + + `); + }); + + it('does not render - higher current step', async () => { + const page = await newSpecPage({ + components: [GcdsStepper], + html: `Section title`, + }); + expect(page.root).toEqualHtml(` + + + + Section title + + `); + }); + + it('does not render - negative current step', async () => { + const page = await newSpecPage({ + components: [GcdsStepper], + html: `Section title`, + }); + expect(page.root).toEqualHtml(` + + + + Section title + + `); + }); + + it('does not render - NaN total steps', async () => { + const page = await newSpecPage({ + components: [GcdsStepper], + html: `Section title`, + }); + expect(page.root).toEqualHtml(` + + + Section title `); }); diff --git a/packages/web/src/index.html b/packages/web/src/index.html index d82d36a13..96295e0e2 100644 --- a/packages/web/src/index.html +++ b/packages/web/src/index.html @@ -219,7 +219,11 @@ Form elements (including stepper and error summary) - + + + Section title + +