@pastweb/components-core 2.0.0 → 2.2.0

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.
@@ -0,0 +1,237 @@
1
+ # DropDown
2
+
3
+ DropDown reveals contextual content from a trigger. Its rendering contract
4
+ follows the WAI-ARIA Disclosure pattern and targets WCAG 2.2 Level AA.
5
+
6
+ ## When to use
7
+
8
+ Use DropDown for short contextual content or a collection of actions associated
9
+ with a trigger. Do not use hover-only activation for essential content. Use a
10
+ dialog when the revealed content requires a modal task or a complex form.
11
+
12
+ ## Props
13
+
14
+ | PropName | Type | Required | Default | Description |
15
+ |---|---|---:|---:|---|
16
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
17
+ | `id` | `string` | no | generated | Supplies the stable base for trigger and content IDs. Generated IDs use the `dropDown` prefix. |
18
+ | `ariaLabel` | `string` | no | `"DropDown"` | Names the trigger when its visible content does not provide a name. Pass an empty string to omit the accessible name. |
19
+ | `arrow` | `boolean` | no | `true` | Displays the visual arrow. |
20
+ | `className` | `string` | no | `""` | Adds classes to the root. |
21
+ | `contentClassName` | `string` | no | `""` | Adds classes to the revealed content. |
22
+ | `offset` | `number` | no | `2` | Sets the renderer positioning offset. |
23
+ | `disabled` | `boolean` | no | `false` | Prevents the content from opening. |
24
+ | `isVisible` | `boolean` | no | `false` | Controls the requested visibility. |
25
+ | `visibleOn` | `'click' \| 'over' \| 'context'` | no | `'click'` | Selects the activation model. Essential content must remain available without hover or a context-menu gesture. |
26
+ | `visibleTarget` | `'element' \| 'pointer'` | no | `'element'` | Selects the positioning reference. `element` uses the trigger. `pointer` uses the pointer position from the trigger event. Keyboard and programmatic opens fall back to the trigger. |
27
+ | `placement` | `Placement` | no | `'bottom'` | Requests placement relative to the trigger. |
28
+ | `hideOnClick` | `boolean` | no | `true` | Closes after activation inside the revealed content. |
29
+ | `hideOnClickOut` | `boolean` | no | `true` | Closes after a click or context-menu activation outside the component. |
30
+ | `hideOnBlur` | `boolean` | no | `true` | Closes when focus leaves the component. |
31
+ | `position` | `'absolute' \| 'fixed'` | no | `'absolute'` | Selects the web positioning strategy. |
32
+ | `clickTimeGap` | `number` | no | `16` | Delays click/context activation in milliseconds. |
33
+ | `onChange` | `(isVisible: boolean) => void` | no | noop | Reports visibility changes. |
34
+ | `onClick` | `() => void` | no | noop | Reports click activation. |
35
+ | `onContext` | `() => void` | no | noop | Reports context-menu activation. |
36
+ | `onClickOut` | `() => void` | no | noop | Reports an outside activation that closed the content. |
37
+ | `onMouseEnter` | `() => void` | no | noop | Reports hover open. |
38
+ | `onMouseLeave` | `() => void` | no | noop | Reports hover close. |
39
+
40
+ ## Mediator API
41
+
42
+ | Property | Type | Description |
43
+ |---|---|---|
44
+ | `id` | `string` | Resolved host ID. Generated IDs use the `dropDown` prefix; an explicit `id` is exposed as `_${id}`. |
45
+
46
+ ## Mediator state
47
+
48
+ | Property | Type | Description |
49
+ |---|---|---|
50
+ | `ariaLabel` | `string \| undefined` | Normalized trigger accessible name. Empty labels are omitted. |
51
+ | `contentId` | `string` | Content relationship ID in the form `content${id}`. |
52
+ | `isDisabled` | `true \| undefined` | Is `true` when the DropDown is disabled; otherwise omitted. Renderers use this value directly for `.is-disabled`, `aria-disabled`, and unavailable keyboard behavior. |
53
+ | `isVisible` | `boolean` | Whether the content is requested open. Renderers use this for `.is-open`, `.is-visible`, `aria-expanded`, and `hidden`. |
54
+ | `triggerId` | `string` | Trigger relationship ID in the form `trigger${id}`. |
55
+
56
+ ## Mediator context
57
+
58
+ Descendant or replacement-slot content reads this API through the renderer context hook. Live getters stay synchronized with mediator state.
59
+
60
+ | Context property | Type | Description |
61
+ |---|---|---|
62
+ | `ariaLabel` | `string \| undefined` | Normalized trigger accessible name. |
63
+ | `arrow` | `boolean` | Whether the decorative arrow is requested. |
64
+ | `cls` | `(...args: ClassValue[]) => string` | Resolves component classes. |
65
+ | `contentClassName` | `string` | Classes added to the revealed content. |
66
+ | `contentId` | `string` | Content relationship ID. |
67
+ | `hide` | `() => void` | Closes the DropDown. Nested menus should call this from item activation. |
68
+ | `id` | `string` | Resolved host ID. |
69
+ | `isDisabled` | `true \| undefined` | Normalized disabled state. |
70
+ | `isVisible` | `boolean` | Whether the content is requested open. |
71
+ | `onClick` | `(event?) => void` | Toggles click-activated content. |
72
+ | `onKeyDown` | `(event) => void` | Handles Enter, Space, and Escape. |
73
+ | `position` | `'absolute' \| 'fixed'` | Web positioning strategy. |
74
+ | `setArrowRef` | `(arrow?) => void` | Stores the mounted arrow reference. |
75
+ | `triggerId` | `string` | Trigger relationship ID. |
76
+
77
+ ## Slots
78
+
79
+ | SlotName | Description | Default content |
80
+ |---|---|---|
81
+ | default | Supplies the trigger content. | Empty. |
82
+ | `header` | Supplies optional leading content inside the revealed panel. | Empty. |
83
+ | `content` | Supplies the main revealed content. | Empty. |
84
+ | `footer` | Supplies optional trailing content inside the revealed panel. | Empty. |
85
+
86
+ ## CSS Variables
87
+
88
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
89
+ |---|---|---|---|---|---|---|
90
+ | `--pw_DropDown_root_disabled_opacity` | `root` | `disabled` | `opacity` | `var(--pw_disabled_opacity)` | Disabled opacity. | Web, Capacitor, NativeScript |
91
+ | `--pw_DropDown_content_default_background-color` | `content` | `default` | `background-color` | `var(--pw_control_background-color)` | Revealed-content background. | Web, Capacitor, NativeScript |
92
+ | `--pw_DropDown_content_default_color` | `content` | `default` | `color` | `var(--pw_text_color)` | Content foreground. | Web, Capacitor, NativeScript |
93
+ | `--pw_DropDown_content_default_border-color` | `content` | `default` | `border-color` | `var(--pw_border-color)` | Content border colour. | Web, Capacitor, NativeScript |
94
+ | `--pw_DropDown_content_default_border-width` | `content` | `default` | `border-width` | `var(--pw_border-width)` | Content border width. | Web, Capacitor, NativeScript |
95
+ | `--pw_DropDown_content_default_border-style` | `content` | `default` | `border-style` | `var(--pw_border-style)` | Content border style. | Web, Capacitor, NativeScript |
96
+ | `--pw_DropDown_content_default_border-radius` | `content` | `default` | `border-radius` | `var(--pw_small_border-radius)` | Content corner radius. | Web, Capacitor, NativeScript |
97
+ | `--pw_DropDown_content_default_padding` | `content` | `default` | `padding` | `var(--pw_padding) 0` | Content spacing. | Web, Capacitor, NativeScript |
98
+ | `--pw_DropDown_content_default_box-shadow` | `content` | `default` | `box-shadow` | shared shadow | Floating-content elevation. | Web, Capacitor |
99
+ | `--pw_DropDown_arrow_default_border-width` | `arrow` | `default` | `border-width` | `1em` | Arrow triangle size. | Web, Capacitor |
100
+ | `--pw_DropDown_target_default_min-width` | `target` | `default` | `min-width` | `2.75em` | Minimum target width. | Web, Capacitor, NativeScript |
101
+ | `--pw_DropDown_target_default_min-height` | `target` | `default` | `min-height` | `2.75em` | Minimum target height. | Web, Capacitor, NativeScript |
102
+ | `--pw_DropDown_target_focus-visible_box-shadow` | `target` | `focus-visible` | `box-shadow` | shared focus shadow | Keyboard focus indicator. | Web, Capacitor |
103
+
104
+ ```xts
105
+ import {
106
+ setContext,
107
+ useBeforeUnmount,
108
+ useMediator,
109
+ useMounted,
110
+ useRef,
111
+ withDefaultProps,
112
+ } from '@pastweb/x';
113
+ import { dropdownMediator } from '@pastweb/components-core/DropDown';
114
+ import { DEFAULT_PROPS, DROPDOWN_CONTEXT_KEY } from './constants';
115
+
116
+ export function DropDown(props: DropDownProps) {
117
+ const p = withDefaultProps<DropDownDefaultProps>(props, DEFAULT_PROPS);
118
+ const m = useMediator<DropDownMediator>(dropdownMediator, p);
119
+ const s = m.state;
120
+ const c = m.getContext();
121
+ const rootRef = useRef(null);
122
+ const targetRef = useRef(null);
123
+ const containerRef = useRef(null);
124
+ const contentRef = useRef(null);
125
+ const arrowRef = useRef(null);
126
+
127
+ setContext(DROPDOWN_CONTEXT_KEY, c);
128
+ useMounted(() => {
129
+ if (
130
+ !rootRef.value
131
+ || !targetRef.value
132
+ || !containerRef.value
133
+ || !contentRef.value
134
+ ) return;
135
+
136
+ m.useMounted(
137
+ rootRef.value,
138
+ targetRef.value,
139
+ containerRef.value,
140
+ contentRef.value,
141
+ );
142
+ if (arrowRef.value) m.setArrowRef(arrowRef.value);
143
+ });
144
+ useBeforeUnmount(m.useBeforeUnmount);
145
+
146
+ return (
147
+ <div
148
+ #ref={rootRef}
149
+ id={m.id}
150
+ class={p.cls('DropDown', p.className, {
151
+ 'is-disabled': s.isDisabled,
152
+ 'is-open': s.isVisible,
153
+ })}
154
+ @click={m.onClick}
155
+ @contextmenu={m.onContext}
156
+ @mouseenter={m.onMouseEnter}
157
+ @mouseleave={m.onMouseLeave}
158
+ @blur={m.onBlur}
159
+ >
160
+ <span
161
+ #ref={targetRef}
162
+ id={s.triggerId}
163
+ class={p.cls('target')}
164
+ role="button"
165
+ tabIndex={s.isDisabled ? -1 : 0}
166
+ aria-label={s.ariaLabel}
167
+ aria-haspopup="true"
168
+ aria-expanded={s.isVisible}
169
+ aria-controls={s.contentId}
170
+ aria-disabled={s.isDisabled}
171
+ @keydown={m.onKeyDown}
172
+ >
173
+ <slot />
174
+ <div
175
+ #ref={containerRef}
176
+ class={p.cls('container', { 'is-visible': s.isVisible })}
177
+ style={{ position: p.position }}
178
+ aria-hidden={!s.isVisible || undefined}
179
+ inert={!s.isVisible || undefined}
180
+ >
181
+ <div
182
+ #ref={contentRef}
183
+ id={s.contentId}
184
+ class={p.cls('content', p.contentClassName)}
185
+ aria-labelledby={s.triggerId}
186
+ @click={m.onClick}
187
+ >
188
+ <slot name="header" />
189
+ <slot name="content" />
190
+ <slot name="footer" />
191
+ </div>
192
+ <div #if={p.arrow} #ref={arrowRef} class={p.cls('arrow')} aria-hidden="true">
193
+ <div class={p.cls('triangle')} />
194
+ </div>
195
+ </div>
196
+ </span>
197
+ </div>
198
+ );
199
+ }
200
+ ```
201
+
202
+ ## Keyboard interaction
203
+
204
+ - `Tab` and `Shift+Tab` move focus to and from the trigger in document order.
205
+ - `Enter` or `Space` on the trigger toggles click-activated content.
206
+ - A click on the trigger hides context-activated content when it is already open.
207
+ - `Escape` closes open content.
208
+
209
+ ## Accessibility notes
210
+
211
+ - The trigger exposes expanded, disabled, and ID relationship state.
212
+ - Closed content is `inert` and `aria-hidden` so it is removed from keyboard and
213
+ accessibility navigation. Do not rely on opacity alone.
214
+ - Hover-only activation is not sufficient for essential content.
215
+ - The decorative arrow is hidden from assistive technology.
216
+ - Shared web styles provide at least a 44 by 44 CSS-pixel target and a visible
217
+ focus indicator.
218
+
219
+ ## Native renderer mapping
220
+
221
+ NativeScript renderers map the trigger to a focusable native control, visibility
222
+ to the native hidden/collapsed state, and expanded/disabled intent to native
223
+ accessibility state. Positioning, outside activation, and animation completion
224
+ remain renderer responsibilities and require iOS and Android device testing.
225
+
226
+ ## Known limitations
227
+
228
+ - Positioning uses web layout measurement. Native renderers must supply an
229
+ equivalent placement adapter.
230
+ - Automated accessibility checks do not by themselves establish WCAG
231
+ conformance.
232
+
233
+ ## Standards
234
+
235
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
236
+ - WAI-ARIA Disclosure pattern: https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
237
+ - GOV.UK Details guidance: https://design-system.service.gov.uk/components/details/
@@ -0,0 +1,224 @@
1
+ # FileUploader
2
+
3
+ FileUploader lets users choose files through a native file picker, with an
4
+ optional dropzone. Its rendering contract uses a native `input type="file"`
5
+ and a visible trigger, and targets WCAG 2.2 Level AA.
6
+
7
+ ## When to use
8
+
9
+ Use a file uploader only when a file is required to complete the task. Provide
10
+ a visible label that describes the file being requested. Do not rely on
11
+ drag-and-drop as the only way to add files; keyboard and assistive-technology
12
+ users must be able to open the native picker.
13
+
14
+ ## Props
15
+
16
+ | PropName | Type | Required | Default | Description |
17
+ |---|---|---:|---:|---|
18
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
19
+ | `accept` | `string` | no | - | Restricts the native picker to matching file types. |
20
+ | `ariaLabel` | `string` | no | `"Upload a file"` | Names the trigger when no visible `label` is supplied. Pass an empty string to omit the accessible name. |
21
+ | `className` | `string` | no | `""` | Adds classes to the wrapper. |
22
+ | `disabled` | `boolean` | no | - | Makes choosing and dropping files unavailable. |
23
+ | `dropzone` | `boolean` | no | - | Enables drop-to-add in addition to the native picker. |
24
+ | `formData` | `FormData` | no | `new FormData()` | Supplies the store used for selected files. |
25
+ | `id` | `string` | no | generated | Supplies the root ID. Generated IDs use the `fileUploader` prefix. |
26
+ | `label` | `string` | no | `""` | Supplies the visible accessible label. |
27
+ | `maxNumber` | `number` | no | `1` | Limits how many files can be stored. |
28
+ | `maxSize` | `number` | no | - | Maximum file size in bytes. Oversized files are reported through `onAddFile` and are not stored. |
29
+ | `multiple` | `boolean` | no | - | Allows the native picker to select more than one file at a time. |
30
+ | `name` | `string` | no | - | Supplies the native file input name. |
31
+ | `tabIndex` | `number` | no | `0` | Controls sequential focus order of the trigger. Prefer the default document order. |
32
+ | `onAddFile` | `(file: File, data: FormData) => void` | no | noop | Reports each file after a successful store, and oversized files that were rejected. |
33
+
34
+ ## Mediator API
35
+
36
+ | Property | Type | Description |
37
+ |---|---|---|
38
+ | `id` | `string` | Resolved host ID. Generated IDs use the `fileUploader` prefix; an explicit `id` is exposed as `_${id}`. |
39
+ | `inputId` | `string` | File input ID derived as `input${id}`. |
40
+ | `labelId` | `string` | Visible label ID derived as `label${id}`. |
41
+
42
+ ## Mediator state
43
+
44
+ | Property | Type | Description |
45
+ |---|---|---|
46
+ | `ariaLabel` | `string \| undefined` | Normalized accessible name when no visible `label` is present. Empty labels are omitted. |
47
+ | `canAddMore` | `boolean` | Is `true` while stored files are under `maxNumber`. |
48
+ | `fileCount` | `number` | Number of stored files. |
49
+ | `files` | `File[]` | File values currently stored in FormData. |
50
+ | `isDisabled` | `true \| undefined` | Is `true` when the uploader is disabled; otherwise omitted. Renderers use this value directly for `.is-disabled`, `disabled`, and `aria-disabled`. |
51
+ | `isDragging` | `true \| undefined` | Is `true` while files are dragged over an enabled dropzone; otherwise omitted. Renderers use this value for `.is-dragging`. |
52
+
53
+ ## Mediator context
54
+
55
+ `fileUploaderMediator.getContext()` exposes live getters consumed by custom
56
+ descendant content. Values that also exist on mediator state are read from that
57
+ normalized state rather than recomputed from raw props.
58
+
59
+ | Property | Type | Description |
60
+ |---|---|---|
61
+ | `accept` | `string \| undefined` | Native picker accept filter. |
62
+ | `canAddMore` | `boolean` | Whether another file can be stored. |
63
+ | `dropzone` | `boolean` | Whether drop-to-add is enabled. |
64
+ | `fileCount` | `number` | Number of stored files. |
65
+ | `files` | `File[]` | Stored File values. |
66
+ | `formData` | `FormData` | The file store. |
67
+ | `id` | `string` | Resolved host ID. |
68
+ | `inputId` | `string` | File input ID. |
69
+ | `isDisabled` | `true \| undefined` | Normalized disabled state. |
70
+ | `labelId` | `string` | Visible label ID. |
71
+ | `maxNumber` | `number` | Stored-file limit. |
72
+ | `maxSize` | `number \| undefined` | Maximum file size in bytes. |
73
+ | `multiple` | `boolean` | Native picker multiple-selection flag. |
74
+ | `removeFile` | `(fileName: string) => void` | Removes a stored file by the FormData key used when it was added. |
75
+
76
+ ## Slots
77
+
78
+ | SlotName | Description | Default content |
79
+ |---|---|---|
80
+ | default | Supplies trigger content such as an icon or “Choose file” text. Must provide an accessible name when `label` and `ariaLabel` are both empty. | Empty. |
81
+
82
+ ## CSS Variables
83
+
84
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
85
+ |---|---|---|---|---|---|---|
86
+ | `--pw_FileUploader_root_default_background-color` | `root` | `default` | `background-color` | `var(--pw_control_background-color)` | Trigger background. | Web, Capacitor, NativeScript |
87
+ | `--pw_FileUploader_root_default_color` | `root` | `default` | `color` | `var(--pw_text_color)` | Trigger text colour. | Web, Capacitor, NativeScript |
88
+ | `--pw_FileUploader_root_default_padding` | `root` | `default` | `padding` | `var(--pw_padding)` | Trigger inner spacing. | Web, Capacitor, NativeScript |
89
+ | `--pw_FileUploader_root_default_margin` | `root` | `default` | `margin` | shared margins | Component spacing. | Web, Capacitor, NativeScript |
90
+ | `--pw_FileUploader_root_default_border-color` | `root` | `default` | `border-color` | `var(--pw_border-color)` | Trigger border colour. | Web, Capacitor, NativeScript |
91
+ | `--pw_FileUploader_root_default_border-width` | `root` | `default` | `border-width` | `var(--pw_border-width)` | Trigger border width. | Web, Capacitor, NativeScript |
92
+ | `--pw_FileUploader_root_default_border-style` | `root` | `default` | `border-style` | `var(--pw_border-style)` | Trigger border style. | Web, Capacitor, NativeScript |
93
+ | `--pw_FileUploader_root_default_border-radius` | `root` | `default` | `border-radius` | `var(--pw_border-radius)` | Trigger corner radius. | Web, Capacitor, NativeScript |
94
+ | `--pw_FileUploader_root_rounded_border-radius` | `root` | `rounded` | `border-radius` | `var(--pw_rounded_border-radius)` | Trigger corner radius for `is-rounded`. | Web, Capacitor, NativeScript |
95
+ | `--pw_FileUploader_root_default_min-height` | `root` | `default` | `min-height` | `2.75em` | Minimum trigger size, 44px at the default font size. | Web, Capacitor, NativeScript |
96
+ | `--pw_FileUploader_root_default_transition-duration` | `root` | `default` | `transition-duration` | `var(--pw_transition-duration)` | Visual state transition duration. | Web, Capacitor; NativeScript adapter |
97
+ | `--pw_FileUploader_root_disabled_opacity` | `root` | `disabled` | `opacity` | `var(--pw_disabled_opacity)` | Disabled opacity. | Web, Capacitor, NativeScript |
98
+ | `--pw_FileUploader_root_highlighted_border-color` | `root` | `highlighted` | `border-color` | `var(--pw_outline_color)` | Drop-target border colour. | Web, Capacitor |
99
+ | `--pw_FileUploader_trigger_default_gap` | `trigger` | `default` | `gap` | `var(--pw_horizontal_padding)` | Space between trigger contents. | Web, Capacitor, NativeScript |
100
+ | `--pw_FileUploader_label_default_color` | `label` | `default` | `color` | `var(--pw_label_text_color)` | Visible label colour. | Web, Capacitor, NativeScript |
101
+ | `--pw_FileUploader_label_default_font-weight` | `label` | `default` | `font-weight` | `var(--pw_semibold_font-weight)` | Visible label weight. | Web, Capacitor, NativeScript |
102
+ | `--pw_FileUploader_color_default_box-shadow` | `color` | `default` | `box-shadow` | `var(--pw_color_box-shadow)` | Focus and drag shadow colour. | Web, Capacitor |
103
+ | `--pw_FileUploader_size_default_box-shadow` | `size` | `default` | `box-shadow` | `var(--pw_size_box-shadow)` | Shadow size. | Web, Capacitor |
104
+
105
+ ```xts
106
+ import {
107
+ setContext,
108
+ useBeforeUnmount,
109
+ useMediator,
110
+ useMounted,
111
+ useRef,
112
+ withDefaultProps,
113
+ } from '@pastweb/x';
114
+ import { fileUploaderMediator } from '@pastweb/components-core/FileUploader';
115
+ import { DEFAULT_PROPS, FILE_UPLOADER_CONTEXT_KEY } from './constants';
116
+
117
+ export function FileUploader(props: FileUploaderProps) {
118
+ const p = withDefaultProps<FileUploaderDefaultProps>(props, DEFAULT_PROPS);
119
+ const m = useMediator<FileUploaderMediator>(fileUploaderMediator, p);
120
+ const s = m.state;
121
+ const inputRef = useRef(null);
122
+ const c = m.getContext();
123
+
124
+ setContext(FILE_UPLOADER_CONTEXT_KEY, c);
125
+ useMounted(() => m.useMounted(inputRef.value));
126
+ useBeforeUnmount(m.useBeforeUnmount);
127
+
128
+ return (
129
+ <div
130
+ id={m.id}
131
+ class={p.cls('FileUploader', p.className, {
132
+ 'is-disabled': s.isDisabled,
133
+ 'is-dragging': s.isDragging,
134
+ 'is-dropzone': p.dropzone,
135
+ 'is-full': !s.canAddMore,
136
+ })}
137
+ aria-disabled={s.isDisabled}
138
+ @dragenter={m.onDragEnter}
139
+ @dragover={m.onDragOver}
140
+ @dragleave={m.onDragLeave}
141
+ @drop={m.onDrop}
142
+ >
143
+ <label #if={p.label} id={m.labelId} class={p.cls('label')} for={m.inputId}>
144
+ {p.label}
145
+ </label>
146
+ <button
147
+ type="button"
148
+ class={p.cls('trigger')}
149
+ disabled={s.isDisabled}
150
+ aria-disabled={s.isDisabled}
151
+ aria-label={s.ariaLabel}
152
+ aria-labelledby={p.label ? m.labelId : undefined}
153
+ tabIndex={p.tabIndex}
154
+ @click={m.onActivate}
155
+ @keydown={m.onKeyDown}
156
+ >
157
+ <slot />
158
+ </button>
159
+ <input
160
+ #ref={inputRef}
161
+ id={m.inputId}
162
+ class={p.cls('inputElement')}
163
+ type="file"
164
+ name={p.name}
165
+ accept={p.accept}
166
+ multiple={p.multiple}
167
+ disabled={s.isDisabled}
168
+ tabIndex={-1}
169
+ @change={m.onChange}
170
+ />
171
+ </div>
172
+ );
173
+ }
174
+ ```
175
+
176
+ Renderers pass native change and drag events through to the mediator. For a
177
+ native change event, the mediator reads the file input from `event.target` and
178
+ falls back to `event.currentTarget`. This supports both native DOM events and
179
+ renderer event wrappers without renderer-specific adapters. The mediator stops
180
+ drag propagation, prevents the browser from opening dropped files, and converts
181
+ `FileList` values to `File[]`.
182
+
183
+ ## Keyboard interaction
184
+
185
+ - `Tab` and `Shift+Tab` move focus to and from the trigger in document order.
186
+ - `Enter` and `Space` open the native file picker when another file can be
187
+ stored.
188
+ - The hidden file input is not in the tab order. Keyboard users operate the
189
+ visible trigger.
190
+ - Drag-and-drop is pointer-only additional behaviour. It must not replace the
191
+ picker.
192
+
193
+ ## Accessibility behaviour
194
+
195
+ - The native file input remains the mechanism that chooses files. The trigger
196
+ is a `type="button"` control that opens that input.
197
+ - A visible `label` names the control through `aria-labelledby`. When no
198
+ visible label is supplied, `ariaLabel` names the trigger. Pass an empty
199
+ `ariaLabel` only when slot content already provides the accessible name.
200
+ - Disabled state is exposed as `.is-disabled`, native `disabled`, and
201
+ `aria-disabled`.
202
+ - Dropzone entry is a visual `is-dragging` state. Do not make dropping the
203
+ only way to add files.
204
+ - IDs used for the host, label, and input are mediator-owned and stable.
205
+
206
+ ## Native renderer mapping
207
+
208
+ NativeScript renderers map the contract to the platform file-picker API, a
209
+ native button or equivalent trigger, disabled state, and native accessibility
210
+ name. Web DOM refs, `FileList`, and drag events are renderer details. Do not
211
+ copy ARIA attributes into native markup.
212
+
213
+ ## Known limitations
214
+
215
+ - `accept` is enforced by the native picker. Dropped files are not filtered by
216
+ `accept` in the mediator.
217
+ - Automated accessibility checks do not by themselves establish WCAG
218
+ conformance.
219
+
220
+ ## Standards
221
+
222
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
223
+ - WAI-ARIA Button pattern: https://www.w3.org/WAI/ARIA/apg/patterns/button/
224
+ - GOV.UK file upload guidance: https://design-system.service.gov.uk/components/file-upload/
@@ -0,0 +1,103 @@
1
+ # Icon
2
+
3
+ Icon renders an inline SVG or image URL from a direct source, local collection,
4
+ or Iconify-compatible API. Its rendering contract targets WCAG 2.2 Level AA.
5
+
6
+ ## When to use
7
+
8
+ Use Icon for a compact visual symbol. Icons are decorative by default. Supply
9
+ `ariaLabel` only when the icon communicates meaning that is not already present
10
+ in nearby text. Do not use an icon as an unlabeled interactive control. Prefer
11
+ a visible text label alongside an icon when the symbol is part of a control.
12
+
13
+ ## Props
14
+
15
+ | PropName | Type | Required | Default | Description |
16
+ |---|---|---:|---:|---|
17
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component classes. |
18
+ | `ariaLabel` | `string` | no | `""` | Names a meaningful icon and gives it the image role. Pass an empty string to keep the icon decorative. |
19
+ | `className` | `string` | no | `""` | Adds classes to the root. |
20
+ | `src` | `string` | no | `""` | Supplies an SVG string, image URL, or data URL. |
21
+ | `name` | `string` | no | `""` | Selects an icon using `collection:icon` syntax. |
22
+ | `collection` | `Collection` | no | `{}` | Supplies trusted local icon sources by collection and name. |
23
+ | `apiUrl` | `string` | no | `https://api.iconify.design` | Selects the Iconify-compatible API endpoint. |
24
+
25
+ SVG strings are injected as markup and therefore must come from trusted sources.
26
+
27
+ ## Mediator state
28
+
29
+ | Property | Type | Description |
30
+ |---|---|---|
31
+ | `ariaHidden` | `true \| undefined` | Is `true` when the icon is decorative; otherwise omitted. Renderers use this value directly for `aria-hidden`. |
32
+ | `ariaLabel` | `string \| undefined` | Normalized accessible name. Empty labels are omitted. |
33
+ | `collectionName` | `string` | Collection segment parsed from `name`. |
34
+ | `iconData` | `IconData \| string` | Inline SVG markup when the source is an SVG string. |
35
+ | `iconName` | `string` | Icon segment parsed from `name`. |
36
+ | `role` | `'img' \| undefined` | Is `img` when `ariaLabel` supplies a name; otherwise omitted. |
37
+ | `src` | `string` | Image URL when the source is not inline SVG. |
38
+
39
+ ## CSS Variables
40
+
41
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
42
+ |---|---|---|---|---|---|---|
43
+ | `--pw_Icon_root_default_color` | `root` | `default` | `color` | `var(--pw_text_strong_color)` | Icon foreground. | Web, Capacitor, NativeScript |
44
+ | `--pw_Icon_graphic_default_width` | `graphic` | `default` | `width` | `1em` | Graphic width. | Web, Capacitor, NativeScript |
45
+ | `--pw_Icon_graphic_default_height` | `graphic` | `default` | `height` | `1em` | Graphic height. | Web, Capacitor, NativeScript |
46
+
47
+ ```xts
48
+ import { useMediator, withDefaultProps } from '@pastweb/x';
49
+ import { iconMediator } from '@pastweb/components-core/Icon';
50
+ import { DEFAULT_PROPS } from './constants';
51
+
52
+ export function Icon(props: IconProps) {
53
+ const p = withDefaultProps<IconDefaultProps>(props, DEFAULT_PROPS);
54
+ const m = useMediator<IconMediator>(iconMediator, p);
55
+ const s = m.state;
56
+
57
+ return (
58
+ <i
59
+ #if={s.iconData || s.src}
60
+ class={p.cls('Icon', p.className)}
61
+ role={s.role}
62
+ aria-label={s.ariaLabel}
63
+ aria-hidden={s.ariaHidden}
64
+ #html={s.iconData || undefined}
65
+ >
66
+ <img #if={s.src} src={s.src} alt="" />
67
+ </i>
68
+ );
69
+ }
70
+ ```
71
+
72
+ ## Keyboard interaction
73
+
74
+ - Icon is not interactive. It does not enter the tab order.
75
+
76
+ ## Accessibility notes
77
+
78
+ - Decorative icons are removed from the accessibility tree with `aria-hidden`.
79
+ - Meaningful icons expose `role="img"` and one accessible name on the root.
80
+ Nested images use empty alternative text to prevent duplicate announcements.
81
+ - The default `ariaLabel` is an empty string, which keeps the icon decorative.
82
+ Pass a task-specific name when the icon is meaningful.
83
+ - Icons used inside controls do not replace the control's visible or accessible
84
+ label.
85
+
86
+ ## Native renderer mapping
87
+
88
+ NativeScript renderers map trusted vector/image sources to supported native image
89
+ views. Decorative icons disable native accessibility exposure; meaningful icons
90
+ map `ariaLabel` intent to `accessibilityLabel` and image-role state. Do not copy
91
+ ARIA attributes into native markup.
92
+
93
+ ## Known limitations
94
+
95
+ - Inline SVG markup must come from trusted sources.
96
+ - Automated accessibility checks do not by themselves establish WCAG
97
+ conformance.
98
+
99
+ ## Standards
100
+
101
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
102
+ - WAI-ARIA Image pattern: https://www.w3.org/WAI/ARIA/apg/patterns/
103
+ - GOV.UK images guidance: https://design-system.service.gov.uk/styles/images/