@charcoal-ui/react 6.0.0-beta.1 → 6.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Icon/index.d.ts +14 -1
- package/dist/components/Icon/index.d.ts.map +1 -1
- package/dist/components/Pagination/index.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/layered.css +1 -3
- package/dist/layered.css.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/css-output.test.ts +1 -2
- package/src/components/DropdownSelector/ListItem/__snapshots__/index.story.storyshot +2 -0
- package/src/components/DropdownSelector/__snapshots__/index.story.storyshot +26 -13
- package/src/components/HintText/__snapshots__/index.story.storyshot +4 -0
- package/src/components/Icon/__snapshots__/index.story.storyshot +2 -0
- package/src/components/Icon/index.browser.test.tsx +280 -0
- package/src/components/Icon/index.test.tsx +94 -0
- package/src/components/Icon/index.tsx +49 -4
- package/src/components/IconButton/__snapshots__/index.story.storyshot +6 -0
- package/src/components/Modal/__snapshots__/index.story.storyshot +12 -2
- package/src/components/MultiSelect/__snapshots__/index.story.storyshot +32 -0
- package/src/components/MultiSelect/index.tsx +1 -1
- package/src/components/Pagination/__snapshots__/index.css.snap +1 -4
- package/src/components/Pagination/index.css +1 -3
- package/src/components/Pagination/index.tsx +2 -1
- package/src/components/TagItem/__snapshots__/index.story.storyshot +2 -0
- package/src/components/TextField/__snapshots__/TextField.story.storyshot +2 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout shift prevention test
|
|
3
|
+
*
|
|
4
|
+
* pixiv-icon Web Component は connectedCallback で Shadow DOM にサイズを設定する。
|
|
5
|
+
* SSR 時は JS が未実行なので、CSS だけでサイズを確保する必要がある。
|
|
6
|
+
*
|
|
7
|
+
* このテストでは以下を検証する:
|
|
8
|
+
*
|
|
9
|
+
* 1. CSS メカニズムの検証 (.charcoal-icon クラス)
|
|
10
|
+
* - plain HTML 要素 に charcoal-icon クラスと --charcoal-icon-size を設定
|
|
11
|
+
* - Web Component を介さず CSS だけでサイズが付くことを確認
|
|
12
|
+
*
|
|
13
|
+
* 2. pixiv-icon:not(:defined) の SSR fallback CSS の検証
|
|
14
|
+
* - iframe (= 子 window では CustomElementRegistry が独立 = pixiv-icon 未登録) を用いて
|
|
15
|
+
* Web Component 登録前の vanilla HTML SSR 状態を再現
|
|
16
|
+
*
|
|
17
|
+
* 3. React Icon コンポーネントの統合テスト
|
|
18
|
+
* - Icon コンポーネントが正しい CSS variable を設定し、
|
|
19
|
+
* WC upgrade 後も同じサイズであることを確認
|
|
20
|
+
*
|
|
21
|
+
* 4. 動的 prop 変更で host + shadow SVG サイズが追従するかの検証
|
|
22
|
+
* - observedAttributes に含まれない size 系 prop が CSS 変数継承で追従することを確認
|
|
23
|
+
*
|
|
24
|
+
* Icon コンポーネントが '@charcoal-ui/icons/css/icon.css' を side-effect import するため、
|
|
25
|
+
* テスト実行時には Vite が自動的にスタイルを document に注入する (本番と同じ挙動)。
|
|
26
|
+
*/
|
|
27
|
+
import iconCssRaw from '@charcoal-ui/icons/css/icon.css?raw'
|
|
28
|
+
import { render } from '@testing-library/react'
|
|
29
|
+
import Icon from '.'
|
|
30
|
+
import HintText from '../HintText'
|
|
31
|
+
import TagItem from '../TagItem'
|
|
32
|
+
|
|
33
|
+
// 本番 icon.css の生テキスト (drift 防止のため文字列複製ではなく実ファイル参照)。
|
|
34
|
+
// iframe テストで pixiv-icon:not(:defined) を検証するために、子 document に注入する。
|
|
35
|
+
const iconCss = iconCssRaw
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* CSS のみと Web Component upgrade 後の両方で
|
|
39
|
+
* 同じサイズになることを保証するためのテストケース
|
|
40
|
+
*/
|
|
41
|
+
const iconSizeTestCases = [
|
|
42
|
+
{ name: '24/Add', expected: 24 },
|
|
43
|
+
{ name: '24/Add', scale: 2, expected: 48 },
|
|
44
|
+
{ name: '24/Add', scale: 3, expected: 72 },
|
|
45
|
+
{ name: '24/Add', fixedSize: 64, expected: 64 },
|
|
46
|
+
{ name: '24/Add', unsafeNonGuidelineScale: 1.5, expected: 36 },
|
|
47
|
+
{ name: 'Inline/Add', expected: 16 },
|
|
48
|
+
{ name: 'Inline/Add', scale: 2, expected: 32 },
|
|
49
|
+
{ name: '16/Add', expected: 16 },
|
|
50
|
+
{ name: '32/BookmarkOff', expected: 32 },
|
|
51
|
+
] as const
|
|
52
|
+
|
|
53
|
+
function getIconSize(container: HTMLElement) {
|
|
54
|
+
const el = container.querySelector('pixiv-icon') as HTMLElement
|
|
55
|
+
const rect = el.getBoundingClientRect()
|
|
56
|
+
return { width: rect.width, height: rect.height }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('charcoal-icon CSS provides correct sizing without Web Component', () => {
|
|
60
|
+
// CSS メカニズムを plain HTML 要素で直接テストする。
|
|
61
|
+
// Web Component を一切介さないため、SSR 時の CSS-only サイジングと等価。
|
|
62
|
+
// Icon を import した時点で side-effect の icon.css が document に注入される。
|
|
63
|
+
|
|
64
|
+
it.each(iconSizeTestCases)(
|
|
65
|
+
'element with --charcoal-icon-size: $expected px has $expected x $expected',
|
|
66
|
+
({ expected }) => {
|
|
67
|
+
const el = document.createElement('span')
|
|
68
|
+
el.className = 'charcoal-icon'
|
|
69
|
+
el.style.setProperty('--charcoal-icon-size', `${expected}px`)
|
|
70
|
+
document.body.appendChild(el)
|
|
71
|
+
try {
|
|
72
|
+
const rect = el.getBoundingClientRect()
|
|
73
|
+
expect(rect.width).toBe(expected)
|
|
74
|
+
expect(rect.height).toBe(expected)
|
|
75
|
+
} finally {
|
|
76
|
+
el.remove()
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('Icon component has correct size with Web Component upgrade', () => {
|
|
83
|
+
it('pixiv-icon IS a registered custom element', () => {
|
|
84
|
+
expect(customElements.get('pixiv-icon')).toBeDefined()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it.each(iconSizeTestCases)(
|
|
88
|
+
'Icon $name (scale=$scale, fixedSize=$fixedSize, unsafeNonGuidelineScale=$unsafeNonGuidelineScale) has $expected x $expected',
|
|
89
|
+
({ name, scale, fixedSize, unsafeNonGuidelineScale, expected }) => {
|
|
90
|
+
const { container } = render(
|
|
91
|
+
<Icon
|
|
92
|
+
name={name}
|
|
93
|
+
scale={scale}
|
|
94
|
+
fixedSize={fixedSize}
|
|
95
|
+
unsafeNonGuidelineScale={unsafeNonGuidelineScale}
|
|
96
|
+
/>,
|
|
97
|
+
)
|
|
98
|
+
const { width, height } = getIconSize(container)
|
|
99
|
+
expect(width).toBe(expected)
|
|
100
|
+
expect(height).toBe(expected)
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
it('HintText icon has 16x16', () => {
|
|
105
|
+
const { container } = render(<HintText>hint</HintText>)
|
|
106
|
+
const { width, height } = getIconSize(container)
|
|
107
|
+
expect(width).toBe(16)
|
|
108
|
+
expect(height).toBe(16)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('TagItem (active) icon has 16x16', () => {
|
|
112
|
+
const { container } = render(<TagItem label="tag" status="active" />)
|
|
113
|
+
const { width, height } = getIconSize(container)
|
|
114
|
+
expect(width).toBe(16)
|
|
115
|
+
expect(height).toBe(16)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('pixiv-icon:not(:defined) reserves correct size with CSS only (vanilla HTML SSR)', () => {
|
|
120
|
+
// React 経由ではなく vanilla HTML で <pixiv-icon> を SSR したケースを再現する。
|
|
121
|
+
// 親 window では pixiv-icon が定義済みなので、未登録状態を作るために iframe を使う。
|
|
122
|
+
// iframe には実 icon.css のみを注入し、@charcoal-ui/icons の JS は import しない。
|
|
123
|
+
|
|
124
|
+
type VanillaCase = {
|
|
125
|
+
title: string
|
|
126
|
+
markup: string
|
|
127
|
+
expected: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const vanillaCases: VanillaCase[] = [
|
|
131
|
+
{
|
|
132
|
+
title: '24/Add (no attrs) → 24px',
|
|
133
|
+
markup: `<pixiv-icon name="24/Add"></pixiv-icon>`,
|
|
134
|
+
expected: 24,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
title: '24/Add scale=2 → 48px',
|
|
138
|
+
markup: `<pixiv-icon name="24/Add" scale="2"></pixiv-icon>`,
|
|
139
|
+
expected: 48,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
title: '24/Add scale=3 → 72px',
|
|
143
|
+
markup: `<pixiv-icon name="24/Add" scale="3"></pixiv-icon>`,
|
|
144
|
+
expected: 72,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
title: '16/Add → 16px',
|
|
148
|
+
markup: `<pixiv-icon name="16/Add"></pixiv-icon>`,
|
|
149
|
+
expected: 16,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
title: '32/BookmarkOff → 32px',
|
|
153
|
+
markup: `<pixiv-icon name="32/BookmarkOff"></pixiv-icon>`,
|
|
154
|
+
expected: 32,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
title: 'Inline/Add → 16px',
|
|
158
|
+
markup: `<pixiv-icon name="Inline/Add"></pixiv-icon>`,
|
|
159
|
+
expected: 16,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
title: 'Inline/Add scale=2 → 32px',
|
|
163
|
+
markup: `<pixiv-icon name="Inline/Add" scale="2"></pixiv-icon>`,
|
|
164
|
+
expected: 32,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
title: 'unknown name (no prefix match) → fallback 24px',
|
|
168
|
+
markup: `<pixiv-icon name="24/SomeName"></pixiv-icon>`,
|
|
169
|
+
expected: 24,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
title: 'non-guideline size via --charcoal-icon-size override → 20px',
|
|
173
|
+
markup: `<pixiv-icon name="24/Add" style="--charcoal-icon-size: 20px"></pixiv-icon>`,
|
|
174
|
+
expected: 20,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
title:
|
|
178
|
+
'non-guideline size for a 20/ prefix (not enumerated in CSS) via --charcoal-icon-size → 20px',
|
|
179
|
+
markup: `<pixiv-icon name="20/Custom" style="--charcoal-icon-size: 20px"></pixiv-icon>`,
|
|
180
|
+
expected: 20,
|
|
181
|
+
},
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
async function withIframe(
|
|
185
|
+
body: string,
|
|
186
|
+
assert: (doc: Document, host: HTMLElement) => void,
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
const iframe = document.createElement('iframe')
|
|
189
|
+
iframe.style.cssText = 'border:0;width:200px;height:200px;'
|
|
190
|
+
// srcdoc を appendChild の前にセットすることで、about:blank の暗黙 load を待たずに
|
|
191
|
+
// 単一の load イベントだけで完了させる (load race 防止)。
|
|
192
|
+
iframe.srcdoc = `<!doctype html><html><head><style>${iconCss}</style></head><body>${body}</body></html>`
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await new Promise<void>((resolve) => {
|
|
196
|
+
iframe.addEventListener('load', () => resolve(), { once: true })
|
|
197
|
+
document.body.appendChild(iframe)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const doc = iframe.contentDocument
|
|
201
|
+
if (!doc) throw new Error('iframe document missing')
|
|
202
|
+
const host = doc.querySelector('pixiv-icon') as HTMLElement | null
|
|
203
|
+
if (!host) throw new Error('pixiv-icon not found in iframe')
|
|
204
|
+
assert(doc, host)
|
|
205
|
+
} finally {
|
|
206
|
+
iframe.remove()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
it.each(vanillaCases)('$title', async ({ markup, expected }) => {
|
|
211
|
+
await withIframe(markup, (doc, host) => {
|
|
212
|
+
// iframe 内では pixiv-icon は登録されていないことを確認 (= :not(:defined) が効く)
|
|
213
|
+
const iframeWindow = doc.defaultView as Window & typeof globalThis
|
|
214
|
+
expect(iframeWindow.customElements.get('pixiv-icon')).toBeUndefined()
|
|
215
|
+
|
|
216
|
+
const rect = host.getBoundingClientRect()
|
|
217
|
+
expect(rect.width).toBe(expected)
|
|
218
|
+
expect(rect.height).toBe(expected)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('Icon reflects dynamic prop changes (no observed-attribute regression)', () => {
|
|
224
|
+
function getShadowSvgSize(container: HTMLElement) {
|
|
225
|
+
const host = container.querySelector('pixiv-icon') as HTMLElement
|
|
226
|
+
const svg = host.shadowRoot?.querySelector('svg') as SVGElement | null
|
|
227
|
+
if (!svg) return null
|
|
228
|
+
const rect = svg.getBoundingClientRect()
|
|
229
|
+
return { width: rect.width, height: rect.height }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
it('updates host + shadow SVG size when only fixedSize changes', async () => {
|
|
233
|
+
const { container, rerender } = render(
|
|
234
|
+
<Icon name="24/Add" fixedSize={20} />,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
expect(getIconSize(container)).toEqual({ width: 20, height: 20 })
|
|
238
|
+
|
|
239
|
+
rerender(<Icon name="24/Add" fixedSize={40} />)
|
|
240
|
+
|
|
241
|
+
expect(getIconSize(container)).toEqual({ width: 40, height: 40 })
|
|
242
|
+
|
|
243
|
+
const shadowSize = getShadowSvgSize(container)
|
|
244
|
+
if (shadowSize !== null) {
|
|
245
|
+
expect(shadowSize).toEqual({ width: 40, height: 40 })
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('updates host + shadow SVG size when only unsafeNonGuidelineScale changes', () => {
|
|
250
|
+
const { container, rerender } = render(
|
|
251
|
+
<Icon name="24/Add" unsafeNonGuidelineScale={1.5} />,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
expect(getIconSize(container)).toEqual({ width: 36, height: 36 })
|
|
255
|
+
|
|
256
|
+
rerender(<Icon name="24/Add" unsafeNonGuidelineScale={2.5} />)
|
|
257
|
+
|
|
258
|
+
expect(getIconSize(container)).toEqual({ width: 60, height: 60 })
|
|
259
|
+
|
|
260
|
+
const shadowSize = getShadowSvgSize(container)
|
|
261
|
+
if (shadowSize !== null) {
|
|
262
|
+
expect(shadowSize).toEqual({ width: 60, height: 60 })
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('updates host + shadow SVG size when only scale changes', () => {
|
|
267
|
+
const { container, rerender } = render(<Icon name="24/Add" scale={1} />)
|
|
268
|
+
|
|
269
|
+
expect(getIconSize(container)).toEqual({ width: 24, height: 24 })
|
|
270
|
+
|
|
271
|
+
rerender(<Icon name="24/Add" scale={3} />)
|
|
272
|
+
|
|
273
|
+
expect(getIconSize(container)).toEqual({ width: 72, height: 72 })
|
|
274
|
+
|
|
275
|
+
const shadowSize = getShadowSvgSize(container)
|
|
276
|
+
if (shadowSize !== null) {
|
|
277
|
+
expect(shadowSize).toEqual({ width: 72, height: 72 })
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import Icon from '.'
|
|
3
|
+
|
|
4
|
+
function queryIcon(container: HTMLElement) {
|
|
5
|
+
const el = container.querySelector('pixiv-icon')
|
|
6
|
+
if (el === null) throw new Error('pixiv-icon not found')
|
|
7
|
+
return el as HTMLElement
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('Icon', () => {
|
|
11
|
+
it('always sets --charcoal-icon-size with calculated size', () => {
|
|
12
|
+
const { container } = render(<Icon name="24/Add" />)
|
|
13
|
+
expect(
|
|
14
|
+
queryIcon(container).style.getPropertyValue('--charcoal-icon-size'),
|
|
15
|
+
).toBe('24px')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('sets --charcoal-icon-size with scale', () => {
|
|
19
|
+
const { container } = render(<Icon name="24/Add" scale={2} />)
|
|
20
|
+
expect(
|
|
21
|
+
queryIcon(container).style.getPropertyValue('--charcoal-icon-size'),
|
|
22
|
+
).toBe('48px')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('sets --charcoal-icon-size with unsafeNonGuidelineScale (deprecated)', () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<Icon name="24/Add" unsafeNonGuidelineScale={1.5} />,
|
|
28
|
+
)
|
|
29
|
+
expect(
|
|
30
|
+
queryIcon(container).style.getPropertyValue('--charcoal-icon-size'),
|
|
31
|
+
).toBe('36px')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('sets --charcoal-icon-size with fixedSize', () => {
|
|
35
|
+
const { container } = render(<Icon name="24/Add" fixedSize={100} />)
|
|
36
|
+
expect(
|
|
37
|
+
queryIcon(container).style.getPropertyValue('--charcoal-icon-size'),
|
|
38
|
+
).toBe('100px')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('passes scale attribute through to pixiv-icon', () => {
|
|
42
|
+
const { container } = render(<Icon name="24/Add" scale={2} />)
|
|
43
|
+
expect(queryIcon(container).getAttribute('scale')).toBe('2')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('passes unsafe-non-guideline-scale attribute to pixiv-icon for hydration', () => {
|
|
47
|
+
const { container } = render(
|
|
48
|
+
<Icon name="24/Add" unsafeNonGuidelineScale={1.5} />,
|
|
49
|
+
)
|
|
50
|
+
expect(
|
|
51
|
+
queryIcon(container).getAttribute('unsafe-non-guideline-scale'),
|
|
52
|
+
).toBe('1.5')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('passes fixed-size attribute to pixiv-icon for hydration', () => {
|
|
56
|
+
const { container } = render(<Icon name="24/Add" fixedSize={100} />)
|
|
57
|
+
expect(queryIcon(container).getAttribute('fixed-size')).toBe('100')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('adds charcoal-icon class', () => {
|
|
61
|
+
const { container } = render(<Icon name="24/Add" />)
|
|
62
|
+
expect(queryIcon(container).classList.contains('charcoal-icon')).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('preserves user className alongside charcoal-icon', () => {
|
|
66
|
+
const { container } = render(<Icon name="24/Add" className="my-icon" />)
|
|
67
|
+
const el = queryIcon(container)
|
|
68
|
+
expect(el.classList.contains('charcoal-icon')).toBe(true)
|
|
69
|
+
expect(el.classList.contains('my-icon')).toBe(true)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('merges user style with SSR CSS variable', () => {
|
|
73
|
+
const { container } = render(
|
|
74
|
+
<Icon name="24/Add" style={{ color: 'red' }} />,
|
|
75
|
+
)
|
|
76
|
+
const el = queryIcon(container)
|
|
77
|
+
expect(el.style.getPropertyValue('--charcoal-icon-size')).toBe('24px')
|
|
78
|
+
expect(el.style.color).toBe('red')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('handles Inline icons', () => {
|
|
82
|
+
const { container } = render(<Icon name="Inline/Add" />)
|
|
83
|
+
expect(
|
|
84
|
+
queryIcon(container).style.getPropertyValue('--charcoal-icon-size'),
|
|
85
|
+
).toBe('16px')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('handles Inline icons with scale=2', () => {
|
|
89
|
+
const { container } = render(<Icon name="Inline/Add" scale={2} />)
|
|
90
|
+
expect(
|
|
91
|
+
queryIcon(container).style.getPropertyValue('--charcoal-icon-size'),
|
|
92
|
+
).toBe('32px')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -1,30 +1,75 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
|
|
3
3
|
import '@charcoal-ui/icons'
|
|
4
|
-
import
|
|
4
|
+
import '@charcoal-ui/icons/css/icon.css'
|
|
5
|
+
import { calcActualSize } from '@charcoal-ui/icons'
|
|
6
|
+
import type { IconSizing, PixivIcon, Props } from '@charcoal-ui/icons'
|
|
5
7
|
|
|
6
8
|
export interface OwnProps {
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated `fixedSize` を利用してください。
|
|
11
|
+
* `attr()` の数値解釈サポートが限定的なため、リセット CSS だけではレイアウトシフトが防げません。
|
|
12
|
+
*/
|
|
7
13
|
unsafeNonGuidelineScale?: number
|
|
14
|
+
/**
|
|
15
|
+
* 固定 px サイズで描画します。ガイドライン外のサイズを利用する場合に推奨される指定方法で、
|
|
16
|
+
* 他のサイズ指定 (`scale` / `unsafeNonGuidelineScale`) よりも常に優先されます。
|
|
17
|
+
*
|
|
18
|
+
* `<Icon>` は同じ値を `--charcoal-icon-size` インライン CSS 変数として自動付与するため、
|
|
19
|
+
* Web Component の hydrate 前後でレイアウトシフトは発生しません。
|
|
20
|
+
*/
|
|
21
|
+
fixedSize?: number
|
|
8
22
|
className?: string
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
export interface IconProps
|
|
12
26
|
extends OwnProps,
|
|
13
27
|
React.PropsWithoutRef<
|
|
14
|
-
Omit<Props, 'class' | 'unsafe-non-guideline-scale' | 'css'>
|
|
28
|
+
Omit<Props, 'class' | 'unsafe-non-guideline-scale' | 'fixed-size' | 'css'>
|
|
15
29
|
> {}
|
|
16
30
|
|
|
17
31
|
const Icon = React.forwardRef<PixivIcon, IconProps>(function IconInner(
|
|
18
|
-
{
|
|
32
|
+
{
|
|
33
|
+
name,
|
|
34
|
+
scale,
|
|
35
|
+
unsafeNonGuidelineScale,
|
|
36
|
+
fixedSize,
|
|
37
|
+
className,
|
|
38
|
+
style: userStyle,
|
|
39
|
+
...rest
|
|
40
|
+
},
|
|
19
41
|
ref,
|
|
20
42
|
) {
|
|
43
|
+
const actualSize = React.useMemo(
|
|
44
|
+
// IconSizing の排他制約は IconProps の型レベルで保証されるため、内部では緩和する
|
|
45
|
+
() =>
|
|
46
|
+
calcActualSize({
|
|
47
|
+
name,
|
|
48
|
+
scale,
|
|
49
|
+
unsafeNonGuidelineScale,
|
|
50
|
+
fixedSize,
|
|
51
|
+
} as { name: string } & IconSizing),
|
|
52
|
+
[name, scale, unsafeNonGuidelineScale, fixedSize],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const style = React.useMemo(
|
|
56
|
+
() =>
|
|
57
|
+
({
|
|
58
|
+
...userStyle,
|
|
59
|
+
'--charcoal-icon-size': `${actualSize}px`,
|
|
60
|
+
}) as React.CSSProperties,
|
|
61
|
+
[actualSize, userStyle],
|
|
62
|
+
)
|
|
63
|
+
|
|
21
64
|
return (
|
|
22
65
|
<pixiv-icon
|
|
23
66
|
ref={ref}
|
|
24
67
|
name={name}
|
|
25
68
|
scale={scale}
|
|
26
69
|
unsafe-non-guideline-scale={unsafeNonGuidelineScale}
|
|
27
|
-
|
|
70
|
+
fixed-size={fixedSize}
|
|
71
|
+
style={style}
|
|
72
|
+
class={`charcoal-icon ${className || ''}`.trim()}
|
|
28
73
|
{...rest}
|
|
29
74
|
/>
|
|
30
75
|
)
|
|
@@ -13,7 +13,9 @@ exports[`Storybook Tests > react/IconButton > Default 1`] = `
|
|
|
13
13
|
title="add"
|
|
14
14
|
>
|
|
15
15
|
<pixiv-icon
|
|
16
|
+
class="charcoal-icon"
|
|
16
17
|
name="16/Add"
|
|
18
|
+
style="--charcoal-icon-size: 16px;"
|
|
17
19
|
/>
|
|
18
20
|
</button>
|
|
19
21
|
</div>
|
|
@@ -32,7 +34,9 @@ exports[`Storybook Tests > react/IconButton > IsActive 1`] = `
|
|
|
32
34
|
data-variant="Default"
|
|
33
35
|
>
|
|
34
36
|
<pixiv-icon
|
|
37
|
+
class="charcoal-icon"
|
|
35
38
|
name="16/Add"
|
|
39
|
+
style="--charcoal-icon-size: 16px;"
|
|
36
40
|
/>
|
|
37
41
|
</button>
|
|
38
42
|
</div>
|
|
@@ -51,7 +55,9 @@ exports[`Storybook Tests > react/IconButton > Overlay 1`] = `
|
|
|
51
55
|
data-variant="Overlay"
|
|
52
56
|
>
|
|
53
57
|
<pixiv-icon
|
|
58
|
+
class="charcoal-icon"
|
|
54
59
|
name="16/Add"
|
|
60
|
+
style="--charcoal-icon-size: 16px;"
|
|
55
61
|
/>
|
|
56
62
|
</button>
|
|
57
63
|
</div>
|
|
@@ -187,8 +187,9 @@ exports[`Storybook Tests > react/Modal > BackgroundScroll 1`] = `
|
|
|
187
187
|
Apple
|
|
188
188
|
</span>
|
|
189
189
|
<pixiv-icon
|
|
190
|
-
class="charcoal-ui-dropdown-selector-icon"
|
|
190
|
+
class="charcoal-icon charcoal-ui-dropdown-selector-icon"
|
|
191
191
|
name="16/Menu"
|
|
192
|
+
style="--charcoal-icon-size: 16px;"
|
|
192
193
|
/>
|
|
193
194
|
</button>
|
|
194
195
|
</div>
|
|
@@ -221,7 +222,9 @@ exports[`Storybook Tests > react/Modal > BackgroundScroll 1`] = `
|
|
|
221
222
|
type="button"
|
|
222
223
|
>
|
|
223
224
|
<pixiv-icon
|
|
225
|
+
class="charcoal-icon"
|
|
224
226
|
name="24/Close"
|
|
227
|
+
style="--charcoal-icon-size: 24px;"
|
|
225
228
|
/>
|
|
226
229
|
</button>
|
|
227
230
|
</div>
|
|
@@ -322,7 +325,9 @@ exports[`Storybook Tests > react/Modal > BottomSheet 1`] = `
|
|
|
322
325
|
type="button"
|
|
323
326
|
>
|
|
324
327
|
<pixiv-icon
|
|
328
|
+
class="charcoal-icon"
|
|
325
329
|
name="24/Close"
|
|
330
|
+
style="--charcoal-icon-size: 24px;"
|
|
326
331
|
/>
|
|
327
332
|
</button>
|
|
328
333
|
</div>
|
|
@@ -515,8 +520,9 @@ exports[`Storybook Tests > react/Modal > Default 1`] = `
|
|
|
515
520
|
Apple
|
|
516
521
|
</span>
|
|
517
522
|
<pixiv-icon
|
|
518
|
-
class="charcoal-ui-dropdown-selector-icon"
|
|
523
|
+
class="charcoal-icon charcoal-ui-dropdown-selector-icon"
|
|
519
524
|
name="16/Menu"
|
|
525
|
+
style="--charcoal-icon-size: 16px;"
|
|
520
526
|
/>
|
|
521
527
|
</button>
|
|
522
528
|
</div>
|
|
@@ -549,7 +555,9 @@ exports[`Storybook Tests > react/Modal > Default 1`] = `
|
|
|
549
555
|
type="button"
|
|
550
556
|
>
|
|
551
557
|
<pixiv-icon
|
|
558
|
+
class="charcoal-icon"
|
|
552
559
|
name="24/Close"
|
|
560
|
+
style="--charcoal-icon-size: 24px;"
|
|
553
561
|
/>
|
|
554
562
|
</button>
|
|
555
563
|
</div>
|
|
@@ -712,7 +720,9 @@ exports[`Storybook Tests > react/Modal > FullBottomSheet 1`] = `
|
|
|
712
720
|
type="button"
|
|
713
721
|
>
|
|
714
722
|
<pixiv-icon
|
|
723
|
+
class="charcoal-icon"
|
|
715
724
|
name="24/Close"
|
|
725
|
+
style="--charcoal-icon-size: 24px;"
|
|
716
726
|
/>
|
|
717
727
|
</button>
|
|
718
728
|
</div>
|