Auto-generate aria-labels for octicons and remove linter rule (#58349)
This commit is contained in:
@@ -52,7 +52,6 @@
|
|||||||
| GHD041 | third-party-action-pinning | Code examples that use third-party actions must always pin to a full length commit SHA | error | feature, actions |
|
| GHD041 | third-party-action-pinning | Code examples that use third-party actions must always pin to a full length commit SHA | error | feature, actions |
|
||||||
| GHD042 | liquid-tag-whitespace | Liquid tags should start and end with one whitespace. Liquid tag arguments should be separated by only one whitespace. | error | liquid, format |
|
| GHD042 | liquid-tag-whitespace | Liquid tags should start and end with one whitespace. Liquid tag arguments should be separated by only one whitespace. | error | liquid, format |
|
||||||
| GHD043 | link-quotation | Internal link titles must not be surrounded by quotations | error | links, url |
|
| GHD043 | link-quotation | Internal link titles must not be surrounded by quotations | error | links, url |
|
||||||
| GHD044 | octicon-aria-labels | Octicons should always have an aria-label attribute even if aria-hidden. | warning | accessibility, octicons |
|
|
||||||
| GHD045 | code-annotation-comment-spacing | Code comments in annotation blocks must have exactly one space after the comment character(s) | warning | code, comments, annotate, spacing |
|
| GHD045 | code-annotation-comment-spacing | Code comments in annotation blocks must have exactly one space after the comment character(s) | warning | code, comments, annotate, spacing |
|
||||||
| GHD046 | outdated-release-phase-terminology | Outdated release phase terminology should be replaced with current GitHub terminology | warning | terminology, consistency, release-phases |
|
| GHD046 | outdated-release-phase-terminology | Outdated release phase terminology should be replaced with current GitHub terminology | warning | terminology, consistency, release-phases |
|
||||||
| GHD047 | table-column-integrity | Tables must have consistent column counts across all rows | warning | tables, accessibility, formatting |
|
| GHD047 | table-column-integrity | Tables must have consistent column counts across all rows | warning | tables, accessibility, formatting |
|
||||||
|
|||||||
69
src/article-api/tests/article-body.ts
Normal file
69
src/article-api/tests/article-body.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { beforeAll, describe, expect, test } from 'vitest'
|
||||||
|
|
||||||
|
import { get } from '@/tests/helpers/e2etest'
|
||||||
|
|
||||||
|
const makeURL = (pathname: string): string =>
|
||||||
|
`/api/article/body?${new URLSearchParams({ pathname })}`
|
||||||
|
|
||||||
|
describe('article body api', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
// If you didn't set the `ROOT` variable, the tests will fail rather
|
||||||
|
// cryptically. So as a warning for engineers running these tests,
|
||||||
|
// alert in case it was accidentally forgotten.
|
||||||
|
if (!process.env.ROOT) {
|
||||||
|
console.warn(
|
||||||
|
'WARNING: The article body tests require the ROOT environment variable to be set to the fixture root',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('happy path', async () => {
|
||||||
|
const res = await get(makeURL('/en/get-started/start-your-journey/hello-world'))
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(res.body).toContain('## Introduction')
|
||||||
|
expect(res.body).toContain('This is just a test.')
|
||||||
|
expect(res.headers['content-type']).toContain('text/markdown')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('octicons auto-generate aria-labels', async () => {
|
||||||
|
const res = await get(makeURL('/en/get-started/start-your-journey/hello-world'))
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
|
||||||
|
// Check that octicons without aria-label get auto-generated ones
|
||||||
|
expect(res.body).toContain('aria-label="check icon"')
|
||||||
|
expect(res.body).toContain('aria-label="git branch icon"')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('octicons with custom aria-labels use the custom value', async () => {
|
||||||
|
const res = await get(makeURL('/en/get-started/start-your-journey/hello-world'))
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
|
||||||
|
// Check that custom aria-labels are preserved
|
||||||
|
expect(res.body).toContain('aria-label="Supported"')
|
||||||
|
expect(res.body).toContain('aria-label="Not supported"')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('octicons with other attributes still get auto-generated aria-labels', async () => {
|
||||||
|
const res = await get(makeURL('/en/get-started/start-your-journey/hello-world'))
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
|
||||||
|
// Check that octicons with width attribute still get aria-labels
|
||||||
|
expect(res.body).toContain('aria-label="rocket icon"')
|
||||||
|
expect(res.body).toContain('width="32"')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a pathname that does not exist', async () => {
|
||||||
|
const res = await get(makeURL('/en/never/heard/of'))
|
||||||
|
expect(res.statusCode).toBe(404)
|
||||||
|
const { error } = JSON.parse(res.body)
|
||||||
|
expect(error).toBe("No page found for '/en/never/heard/of'")
|
||||||
|
})
|
||||||
|
|
||||||
|
test('non-article pages return error', async () => {
|
||||||
|
// Index pages are not articles and should not be renderable
|
||||||
|
const res = await get(makeURL('/en/get-started'))
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
const { error } = JSON.parse(res.body)
|
||||||
|
expect(error).toContain("isn't yet available in markdown")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -42,7 +42,6 @@ import { tableColumnIntegrity } from '@/content-linter/lib/linting-rules/table-c
|
|||||||
import { thirdPartyActionPinning } from '@/content-linter/lib/linting-rules/third-party-action-pinning'
|
import { thirdPartyActionPinning } from '@/content-linter/lib/linting-rules/third-party-action-pinning'
|
||||||
import { liquidTagWhitespace } from '@/content-linter/lib/linting-rules/liquid-tag-whitespace'
|
import { liquidTagWhitespace } from '@/content-linter/lib/linting-rules/liquid-tag-whitespace'
|
||||||
import { linkQuotation } from '@/content-linter/lib/linting-rules/link-quotation'
|
import { linkQuotation } from '@/content-linter/lib/linting-rules/link-quotation'
|
||||||
import { octiconAriaLabels } from '@/content-linter/lib/linting-rules/octicon-aria-labels'
|
|
||||||
import { liquidIfversionVersions } from '@/content-linter/lib/linting-rules/liquid-ifversion-versions'
|
import { liquidIfversionVersions } from '@/content-linter/lib/linting-rules/liquid-ifversion-versions'
|
||||||
import { outdatedReleasePhaseTerminology } from '@/content-linter/lib/linting-rules/outdated-release-phase-terminology'
|
import { outdatedReleasePhaseTerminology } from '@/content-linter/lib/linting-rules/outdated-release-phase-terminology'
|
||||||
import { frontmatterVersionsWhitespace } from '@/content-linter/lib/linting-rules/frontmatter-versions-whitespace'
|
import { frontmatterVersionsWhitespace } from '@/content-linter/lib/linting-rules/frontmatter-versions-whitespace'
|
||||||
@@ -105,7 +104,7 @@ export const gitHubDocsMarkdownlint = {
|
|||||||
thirdPartyActionPinning, // GHD041
|
thirdPartyActionPinning, // GHD041
|
||||||
liquidTagWhitespace, // GHD042
|
liquidTagWhitespace, // GHD042
|
||||||
linkQuotation, // GHD043
|
linkQuotation, // GHD043
|
||||||
octiconAriaLabels, // GHD044
|
// GHD044 removed - octicon aria-labels are now auto-generated
|
||||||
codeAnnotationCommentSpacing, // GHD045
|
codeAnnotationCommentSpacing, // GHD045
|
||||||
outdatedReleasePhaseTerminology, // GHD046
|
outdatedReleasePhaseTerminology, // GHD046
|
||||||
tableColumnIntegrity, // GHD047
|
tableColumnIntegrity, // GHD047
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { TokenKind } from 'liquidjs'
|
|
||||||
import { getLiquidTokens, getPositionData } from '../helpers/liquid-utils'
|
|
||||||
import { addFixErrorDetail } from '../helpers/utils'
|
|
||||||
import type { RuleParams, RuleErrorCallback, Rule } from '../../types'
|
|
||||||
/*
|
|
||||||
Octicons should always have an aria-label attribute even if aria hidden. For example:
|
|
||||||
|
|
||||||
DO use aria-label
|
|
||||||
{% octicon "alert" aria-label="alert" %}
|
|
||||||
{% octicon "alert" aria-label="alert" aria-hidden="true" %}
|
|
||||||
{% octicon "alert" aria-label="alert" aria-hidden="true" class="foo" %}
|
|
||||||
|
|
||||||
This is necessary for copilot to be able to recognize the svgs correctly when using our API.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const octiconAriaLabels: Rule = {
|
|
||||||
names: ['GHD044', 'octicon-aria-labels'],
|
|
||||||
description: 'Octicons should always have an aria-label attribute even if aria-hidden.',
|
|
||||||
tags: ['accessibility', 'octicons'],
|
|
||||||
parser: 'markdownit',
|
|
||||||
function: (params: RuleParams, onError: RuleErrorCallback) => {
|
|
||||||
const content = params.lines.join('\n')
|
|
||||||
// Using 'any' type for tokens as getLiquidTokens returns tokens from liquid-utils.ts which lacks type definitions
|
|
||||||
const tokens = getLiquidTokens(content)
|
|
||||||
.filter((token: any) => token.kind === TokenKind.Tag)
|
|
||||||
.filter((token: any) => token.name === 'octicon')
|
|
||||||
|
|
||||||
for (const token of tokens) {
|
|
||||||
const { lineNumber, column, length } = getPositionData(token, params.lines)
|
|
||||||
|
|
||||||
const hasAriaLabel = token.args.includes('aria-label=')
|
|
||||||
|
|
||||||
if (!hasAriaLabel) {
|
|
||||||
const range = [column, length]
|
|
||||||
|
|
||||||
const octiconNameMatch = token.args.match(/["']([^"']+)["']/)
|
|
||||||
const octiconName = octiconNameMatch ? octiconNameMatch[1] : 'icon'
|
|
||||||
const originalContent = token.content
|
|
||||||
const fixedContent = `${originalContent} aria-label="${octiconName}"`
|
|
||||||
|
|
||||||
addFixErrorDetail(
|
|
||||||
onError,
|
|
||||||
lineNumber,
|
|
||||||
`octicon should have an aria-label even if aria hidden. Try using 'aria-label=${octiconName}'`,
|
|
||||||
token.content,
|
|
||||||
range,
|
|
||||||
{
|
|
||||||
lineNumber,
|
|
||||||
editColumn: column,
|
|
||||||
deleteCount: length,
|
|
||||||
insertText: `{% ${fixedContent} %}`,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -162,12 +162,7 @@ const githubDocsConfig = {
|
|||||||
'partial-markdown-files': true,
|
'partial-markdown-files': true,
|
||||||
'yml-files': true,
|
'yml-files': true,
|
||||||
},
|
},
|
||||||
'octicon-aria-labels': {
|
// GHD044 removed - octicon aria-labels are now auto-generated
|
||||||
// GHD044
|
|
||||||
severity: 'warning',
|
|
||||||
'partial-markdown-files': true,
|
|
||||||
'yml-files': true,
|
|
||||||
},
|
|
||||||
'code-annotation-comment-spacing': {
|
'code-annotation-comment-spacing': {
|
||||||
// GHD045
|
// GHD045
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
import { describe, expect, test } from 'vitest'
|
|
||||||
import { octiconAriaLabels } from '../../lib/linting-rules/octicon-aria-labels'
|
|
||||||
|
|
||||||
interface ErrorInfo {
|
|
||||||
lineNumber: number
|
|
||||||
detail?: string
|
|
||||||
context?: string
|
|
||||||
range?: [number, number]
|
|
||||||
fixInfo?: any // Matches RuleErrorCallback signature - fixInfo structure varies by rule
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('octicon-aria-labels', () => {
|
|
||||||
const rule = octiconAriaLabels
|
|
||||||
|
|
||||||
// Helper to create onError callback that captures errors
|
|
||||||
function createErrorCollector() {
|
|
||||||
const errors: ErrorInfo[] = []
|
|
||||||
// Using any because the actual rule implementation calls onError with an object,
|
|
||||||
// not individual parameters as defined in RuleErrorCallback
|
|
||||||
const onError = (errorInfo: any) => {
|
|
||||||
errors.push(errorInfo)
|
|
||||||
}
|
|
||||||
return { errors, onError }
|
|
||||||
}
|
|
||||||
|
|
||||||
test('detects octicon without aria-label', () => {
|
|
||||||
const { errors, onError } = createErrorCollector()
|
|
||||||
|
|
||||||
const content = ['This is a test with an octicon:', '{% octicon "alert" %}', 'Some more text.']
|
|
||||||
|
|
||||||
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
|
|
||||||
|
|
||||||
expect(errors.length).toBe(1)
|
|
||||||
expect(errors[0].lineNumber).toBe(2)
|
|
||||||
expect(errors[0].detail).toContain('aria-label=alert')
|
|
||||||
expect(errors[0].fixInfo.insertText).toContain('aria-label="alert"')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('ignores octicons with aria-label', () => {
|
|
||||||
const { errors, onError } = createErrorCollector()
|
|
||||||
|
|
||||||
const content = [
|
|
||||||
'This is a test with a proper octicon:',
|
|
||||||
'{% octicon "alert" aria-label="alert" %}',
|
|
||||||
'Some more text.',
|
|
||||||
]
|
|
||||||
|
|
||||||
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
|
|
||||||
|
|
||||||
expect(errors.length).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('detects multiple octicons without aria-label', () => {
|
|
||||||
const { errors, onError } = createErrorCollector()
|
|
||||||
|
|
||||||
const content = [
|
|
||||||
'This is a test with multiple octicons:',
|
|
||||||
'{% octicon "alert" %}',
|
|
||||||
'Some text in between.',
|
|
||||||
'{% octicon "check" %}',
|
|
||||||
'More text.',
|
|
||||||
]
|
|
||||||
|
|
||||||
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
|
|
||||||
|
|
||||||
expect(errors.length).toBe(2)
|
|
||||||
expect(errors[0].lineNumber).toBe(2)
|
|
||||||
expect(errors[0].detail).toContain('aria-label=alert')
|
|
||||||
expect(errors[1].lineNumber).toBe(4)
|
|
||||||
expect(errors[1].detail).toContain('aria-label=check')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('ignores non-octicon liquid tags', () => {
|
|
||||||
const { errors, onError } = createErrorCollector()
|
|
||||||
|
|
||||||
const content = [
|
|
||||||
'This is a test with non-octicon tags:',
|
|
||||||
'{% data foo.bar %}',
|
|
||||||
'{% ifversion fpt %}',
|
|
||||||
'Some text.',
|
|
||||||
'{% endif %}',
|
|
||||||
]
|
|
||||||
|
|
||||||
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
|
|
||||||
|
|
||||||
expect(errors.length).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('suggests correct fix for octicon with other attributes', () => {
|
|
||||||
const { errors, onError } = createErrorCollector()
|
|
||||||
|
|
||||||
const content = [
|
|
||||||
'This is a test with an octicon with other attributes:',
|
|
||||||
'{% octicon "plus" aria-hidden="true" class="foo" %}',
|
|
||||||
'Some more text.',
|
|
||||||
]
|
|
||||||
|
|
||||||
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
|
|
||||||
|
|
||||||
expect(errors.length).toBe(1)
|
|
||||||
expect(errors[0].lineNumber).toBe(2)
|
|
||||||
expect(errors[0].fixInfo.insertText).toContain('aria-label="plus"')
|
|
||||||
expect(errors[0].fixInfo.insertText).toContain('aria-hidden="true"')
|
|
||||||
expect(errors[0].fixInfo.insertText).toContain('class="foo"')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('handles octicons with unusual spacing', () => {
|
|
||||||
const { errors, onError } = createErrorCollector()
|
|
||||||
|
|
||||||
const content = [
|
|
||||||
'This is a test with unusual spacing:',
|
|
||||||
'{% octicon "x" %}',
|
|
||||||
'Some more text.',
|
|
||||||
]
|
|
||||||
|
|
||||||
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
|
|
||||||
|
|
||||||
expect(errors.length).toBe(1)
|
|
||||||
expect(errors[0].lineNumber).toBe(2)
|
|
||||||
expect(errors[0].detail).toContain('aria-label=x')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('handles octicons split across multiple lines', () => {
|
|
||||||
const { errors, onError } = createErrorCollector()
|
|
||||||
|
|
||||||
const content = [
|
|
||||||
'This is a test with a multi-line octicon:',
|
|
||||||
'{% octicon "chevron-down"',
|
|
||||||
' class="dropdown-menu-icon"',
|
|
||||||
'%}',
|
|
||||||
'Some more text.',
|
|
||||||
]
|
|
||||||
|
|
||||||
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
|
|
||||||
|
|
||||||
expect(errors.length).toBe(1)
|
|
||||||
expect(errors[0].detail).toContain('aria-label=chevron-down')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('falls back to "icon" when octicon name cannot be determined', () => {
|
|
||||||
const { errors, onError } = createErrorCollector()
|
|
||||||
|
|
||||||
const content = [
|
|
||||||
'This is a test with a malformed octicon:',
|
|
||||||
'{% octicon variable %}',
|
|
||||||
'Some more text.',
|
|
||||||
]
|
|
||||||
|
|
||||||
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
|
|
||||||
|
|
||||||
expect(errors.length).toBe(1)
|
|
||||||
expect(errors[0].detail).toContain('aria-label=icon')
|
|
||||||
expect(errors[0].fixInfo.insertText).toContain('aria-label="icon"')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -24,7 +24,10 @@ const SyntaxHelp = 'Syntax Error in tag \'octicon\' - Valid syntax: octicon "<na
|
|||||||
* Uses the octicons library to render the chosen icon. Also
|
* Uses the octicons library to render the chosen icon. Also
|
||||||
* supports passing attributes like `width="64"`.
|
* supports passing attributes like `width="64"`.
|
||||||
*
|
*
|
||||||
* {% octicon "check" %}
|
* If no aria-label is provided, a default one will be auto-generated
|
||||||
|
* based on the icon name (e.g., "check icon", "git-branch icon").
|
||||||
|
*
|
||||||
|
* {% octicon "check" %} <!-- auto-generates aria-label="check icon" -->
|
||||||
* {% octicon "check" width="64" aria-label="Example label" %}
|
* {% octicon "check" width="64" aria-label="Example label" %}
|
||||||
*/
|
*/
|
||||||
const Octicon: LiquidTag = {
|
const Octicon: LiquidTag = {
|
||||||
@@ -70,6 +73,13 @@ const Octicon: LiquidTag = {
|
|||||||
throw new Error(`Octicon ${this.icon} does not exist`)
|
throw new Error(`Octicon ${this.icon} does not exist`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-generate aria-label if not provided
|
||||||
|
// Replace non-alphanumeric characters with spaces and append " icon"
|
||||||
|
if (!this.options['aria-label']) {
|
||||||
|
const defaultLabel = `${this.icon.toLowerCase().replace(/[^a-z0-9]+/gi, ' ')} icon`
|
||||||
|
this.options['aria-label'] = defaultLabel
|
||||||
|
}
|
||||||
|
|
||||||
const result: string = octicons[this.icon].toSVG(this.options)
|
const result: string = octicons[this.icon].toSVG(this.options)
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,4 +42,34 @@ describe('octicon tag', () => {
|
|||||||
'Octicon pizza-patrol does not exist',
|
'Octicon pizza-patrol does not exist',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('auto-generates aria-label when not provided', async () => {
|
||||||
|
const actual = await renderContent('{% octicon "check" %}')
|
||||||
|
expect(actual).toContain('<svg ')
|
||||||
|
expect(actual).toContain('class="octicon octicon-check"')
|
||||||
|
expect(actual).toContain('aria-label="check icon"')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('auto-generates aria-label with spaces for hyphenated icon names', async () => {
|
||||||
|
const actual = await renderContent('{% octicon "git-branch" %}')
|
||||||
|
expect(actual).toContain('<svg ')
|
||||||
|
expect(actual).toContain('class="octicon octicon-git-branch"')
|
||||||
|
expect(actual).toContain('aria-label="git branch icon"')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('uses custom aria-label instead of auto-generated one when provided', async () => {
|
||||||
|
const actual = await renderContent('{% octicon "check" aria-label="Supported" %}')
|
||||||
|
expect(actual).toContain('<svg ')
|
||||||
|
expect(actual).toContain('class="octicon octicon-check"')
|
||||||
|
expect(actual).toContain('aria-label="Supported"')
|
||||||
|
expect(actual).not.toContain('aria-label="check icon"')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('auto-generates aria-label even when other attributes are provided', async () => {
|
||||||
|
const actual = await renderContent('{% octicon "filter" width="32" %}')
|
||||||
|
expect(actual).toContain('<svg ')
|
||||||
|
expect(actual).toContain('class="octicon octicon-filter"')
|
||||||
|
expect(actual).toContain('aria-label="filter icon"')
|
||||||
|
expect(actual).toContain('width="32"')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,3 +33,17 @@ like "Enterprise Server X.Y". It should change the above sentence.
|
|||||||
## Use of a reusable that might have auto-title links
|
## Use of a reusable that might have auto-title links
|
||||||
|
|
||||||
{% data reusables.gated-features.more-info %}
|
{% data reusables.gated-features.more-info %}
|
||||||
|
|
||||||
|
## Octicons for testing
|
||||||
|
|
||||||
|
This section tests octicon rendering with auto-generated aria-labels.
|
||||||
|
|
||||||
|
Here's a check icon without aria-label: {% octicon "check" %}
|
||||||
|
|
||||||
|
Here's a git-branch icon without aria-label: {% octicon "git-branch" %}
|
||||||
|
|
||||||
|
Here's a check icon with custom aria-label: {% octicon "check" aria-label="Supported" %}
|
||||||
|
|
||||||
|
Here's an x icon with custom aria-label: {% octicon "x" aria-label="Not supported" %}
|
||||||
|
|
||||||
|
Here's a rocket icon with width attribute: {% octicon "rocket" width="32" %}
|
||||||
|
|||||||
Reference in New Issue
Block a user