- I made both surfaces friendlier to screen readers.

CMS changes landed in 
[layout.php](/Users/tyemeclifford/Documents/GH/blog/themes/neon/layout.php), 
[style.css](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/style.css), 
[app.js](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/app.js), 
and 
[lone-embed.php](/Users/tyemeclifford/Documents/GH/blog/lone-embed.php): 
skip link, named search/forms, current page state, live notices/empty 
states, labelled media/embed controls, visible focus states, theme 
control pressed states, and a focus-trapped/lightbox dialog with proper 
ARIA.
Mac app changes landed in 
[main.m](/Users/tyemeclifford/Documents/GH/blog/macclient/AppKitClient/main.m), 
and I rebuilt 
[TCMS](/Users/tyemeclifford/Documents/GH/blog/macclient/TCMS.app/Contents/MacOS/TCMS): 
AppKit controls/tables/fields/previews now get accessibility 
labels/help/value text, workspace sections are named, media/comment 
tables expose useful context, and status messages are announced via 
macOS accessibility notifications.
This commit is contained in:
Ty Clifford
2026-07-05 23:36:27 -04:00
parent 0b28e719ee
commit 75d38d4eb8
6 changed files with 298 additions and 64 deletions
+2 -1
View File
@@ -70,8 +70,9 @@ function e(string $value): string
</style> </style>
</head> </head>
<body> <body>
<video controls playsinline preload="metadata"<?php if ($poster !== ''): ?> poster="<?= e($poster) ?>"<?php endif; ?>> <video controls playsinline preload="metadata" aria-label="<?= e($title !== '' ? $title : 'Embedded video') ?>"<?php if ($poster !== ''): ?> poster="<?= e($poster) ?>"<?php endif; ?>>
<source src="<?= e($file) ?>" type="<?= e(media_type($file)) ?>"> <source src="<?= e($file) ?>" type="<?= e(media_type($file)) ?>">
Your browser does not support the video element.
</video> </video>
</body> </body>
</html> </html>
+150 -22
View File
@@ -60,6 +60,25 @@ static NSString *TCMSFormatBytes(id value) {
: [NSString stringWithFormat:@"%.1f %@", amount, units[unitIndex]]; : [NSString stringWithFormat:@"%.1f %@", amount, units[unitIndex]];
} }
static void TCMSSetAccessibility(NSView *view, NSString *label, NSString *help) {
if (!view) {
return;
}
if (label.length > 0) {
[view setAccessibilityLabel:label];
}
if (help.length > 0) {
[view setAccessibilityHelp:help];
}
}
static void TCMSSetAccessibilityValue(NSView *view, NSString *value) {
if (!view) {
return;
}
[view setAccessibilityValue:value ?: @""];
}
@interface TCMSMarkdownTextView : NSTextView @interface TCMSMarkdownTextView : NSTextView
@end @end
@@ -383,6 +402,7 @@ static NSString *TCMSFormatBytes(id value) {
self.window.minSize = NSMakeSize(1040, 680); self.window.minSize = NSMakeSize(1040, 680);
self.window.delegate = self; self.window.delegate = self;
self.window.releasedWhenClosed = NO; self.window.releasedWhenClosed = NO;
TCMSSetAccessibility(self.window.contentView, @"TCMS workspace", @"Manage posts, pages, media, comments, and settings.");
[self.window center]; [self.window center];
NSView *content = self.window.contentView; NSView *content = self.window.contentView;
@@ -390,10 +410,12 @@ static NSString *TCMSFormatBytes(id value) {
sidebar.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin; sidebar.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin;
sidebar.wantsLayer = YES; sidebar.wantsLayer = YES;
sidebar.layer.backgroundColor = [NSColor colorWithCalibratedWhite:0.08 alpha:1].CGColor; sidebar.layer.backgroundColor = [NSColor colorWithCalibratedWhite:0.08 alpha:1].CGColor;
TCMSSetAccessibility(sidebar, @"Navigation and status", @"Choose a TCMS section and hear current status messages.");
[content addSubview:sidebar]; [content addSubview:sidebar];
NSTextField *brand = [self label:@"TCMS" frame:NSMakeRect(18, 680, 154, 34) font:[NSFont boldSystemFontOfSize:28]]; NSTextField *brand = [self label:@"TCMS" frame:NSMakeRect(18, 680, 154, 34) font:[NSFont boldSystemFontOfSize:28]];
brand.textColor = NSColor.whiteColor; brand.textColor = NSColor.whiteColor;
TCMSSetAccessibility(brand, @"TCMS", @"Ty Clifford's Content Management System.");
[sidebar addSubview:brand]; [sidebar addSubview:brand];
NSArray *buttons = @[ NSArray *buttons = @[
@@ -410,6 +432,7 @@ static NSString *TCMSFormatBytes(id value) {
button.frame = NSMakeRect(18, y, 154, 36); button.frame = NSMakeRect(18, y, 154, 36);
button.bezelStyle = NSBezelStyleTexturedRounded; button.bezelStyle = NSBezelStyleTexturedRounded;
button.autoresizingMask = NSViewMinYMargin; button.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(button, [NSString stringWithFormat:@"Show %@", entry[0]], [NSString stringWithFormat:@"Switch to the %@ section.", entry[0]]);
[sidebar addSubview:button]; [sidebar addSubview:button];
y -= 46; y -= 46;
} }
@@ -418,10 +441,13 @@ static NSString *TCMSFormatBytes(id value) {
self.statusLabel.textColor = [NSColor colorWithCalibratedWhite:0.78 alpha:1]; self.statusLabel.textColor = [NSColor colorWithCalibratedWhite:0.78 alpha:1];
self.statusLabel.lineBreakMode = NSLineBreakByWordWrapping; self.statusLabel.lineBreakMode = NSLineBreakByWordWrapping;
self.statusLabel.autoresizingMask = NSViewMaxYMargin; self.statusLabel.autoresizingMask = NSViewMaxYMargin;
TCMSSetAccessibility(self.statusLabel, @"Status", @"Latest TCMS status message.");
TCMSSetAccessibilityValue(self.statusLabel, @"Ready");
[sidebar addSubview:self.statusLabel]; [sidebar addSubview:self.statusLabel];
self.workspace = [[NSView alloc] initWithFrame:NSMakeRect(190, 0, 990, 740)]; self.workspace = [[NSView alloc] initWithFrame:NSMakeRect(190, 0, 990, 740)];
self.workspace.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; self.workspace.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
TCMSSetAccessibility(self.workspace, @"Posts section", @"Main TCMS editing workspace.");
[content addSubview:self.workspace]; [content addSubview:self.workspace];
[self.window makeKeyAndOrderFront:nil]; [self.window makeKeyAndOrderFront:nil];
} }
@@ -504,6 +530,7 @@ static NSString *TCMSFormatBytes(id value) {
label.bezeled = NO; label.bezeled = NO;
label.drawsBackground = NO; label.drawsBackground = NO;
label.autoresizingMask = NSViewMinYMargin; label.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(label, text, nil);
return label; return label;
} }
@@ -512,6 +539,7 @@ static NSString *TCMSFormatBytes(id value) {
field.placeholderString = placeholder; field.placeholderString = placeholder;
field.delegate = self; field.delegate = self;
field.autoresizingMask = NSViewMinYMargin; field.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(field, placeholder, nil);
return field; return field;
} }
@@ -546,6 +574,24 @@ static NSString *TCMSFormatBytes(id value) {
self.mediaPreviewURLString = nil; self.mediaPreviewURLString = nil;
} }
- (void)announceAccessibilityMessage:(NSString *)message {
if (message.length == 0) {
return;
}
NSDictionary *info = @{
NSAccessibilityAnnouncementKey: message,
NSAccessibilityPriorityKey: @(NSAccessibilityPriorityMedium)
};
NSAccessibilityPostNotificationWithUserInfo(self.window ?: NSApp, NSAccessibilityAnnouncementRequestedNotification, info);
}
- (void)configureWorkspaceAccessibilityWithTitle:(NSString *)title heading:(NSView *)heading {
NSString *label = [NSString stringWithFormat:@"%@ section", title.length > 0 ? title : @"TCMS"];
TCMSSetAccessibility(self.workspace, label, @"Main TCMS workspace.");
NSAccessibilityPostNotification(heading ?: self.workspace, NSAccessibilityLayoutChangedNotification);
}
- (void)showContentSection:(NSString *)section { - (void)showContentSection:(NSString *)section {
self.section = section; self.section = section;
[self clearWorkspace]; [self clearWorkspace];
@@ -554,36 +600,51 @@ static NSString *TCMSFormatBytes(id value) {
NSTextField *heading = [self label:[section capitalizedString] frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]]; NSTextField *heading = [self label:[section capitalizedString] frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]];
[self.workspace addSubview:heading]; [self.workspace addSubview:heading];
[self addButton:@"Refresh" frame:NSMakeRect(210, 690, 78, 30) action:@selector(refreshCurrentSection)]; [self configureWorkspaceAccessibilityWithTitle:[section capitalizedString] heading:heading];
[self addButton:@"New" frame:NSMakeRect(296, 690, 58, 30) action:@selector(newContent)]; NSButton *refreshButton = [self addButton:@"Refresh" frame:NSMakeRect(210, 690, 78, 30) action:@selector(refreshCurrentSection)];
[self addButton:@"Draft" frame:NSMakeRect(362, 690, 70, 30) action:@selector(saveDraft)]; NSButton *newButton = [self addButton:@"New" frame:NSMakeRect(296, 690, 58, 30) action:@selector(newContent)];
[self addButton:@"Update" frame:NSMakeRect(440, 690, 76, 30) action:@selector(updateContent)]; NSButton *draftButton = [self addButton:@"Draft" frame:NSMakeRect(362, 690, 70, 30) action:@selector(saveDraft)];
[self addButton:@"Publish" frame:NSMakeRect(524, 690, 78, 30) action:@selector(publishContent)]; NSButton *updateButton = [self addButton:@"Update" frame:NSMakeRect(440, 690, 76, 30) action:@selector(updateContent)];
[self addButton:@"Upload Media" frame:NSMakeRect(610, 690, 118, 30) action:@selector(uploadEditorMedia)]; NSButton *publishButton = [self addButton:@"Publish" frame:NSMakeRect(524, 690, 78, 30) action:@selector(publishContent)];
[self addButton:@"Delete" frame:NSMakeRect(736, 690, 70, 30) action:@selector(deleteContent)]; NSButton *uploadMediaButton = [self addButton:@"Upload Media" frame:NSMakeRect(610, 690, 118, 30) action:@selector(uploadEditorMedia)];
NSButton *deleteButton = [self addButton:@"Delete" frame:NSMakeRect(736, 690, 70, 30) action:@selector(deleteContent)];
TCMSSetAccessibility(refreshButton, [NSString stringWithFormat:@"Refresh %@ list", section], nil);
TCMSSetAccessibility(newButton, [NSString stringWithFormat:@"Create new %@", type], nil);
TCMSSetAccessibility(draftButton, @"Save current content as draft", nil);
TCMSSetAccessibility(updateButton, @"Update current content", nil);
TCMSSetAccessibility(publishButton, @"Publish current content", nil);
TCMSSetAccessibility(uploadMediaButton, @"Upload media and insert it in the editor", nil);
TCMSSetAccessibility(deleteButton, @"Delete current content", @"Deletes the selected post or page after confirmation.");
NSScrollView *listScroll = [self buildTableWithFrame:NSMakeRect(20, 56, 280, 614) columns:@[@"title", @"status"]]; NSScrollView *listScroll = [self buildTableWithFrame:NSMakeRect(20, 56, 280, 614) columns:@[@"title", @"status"]];
listScroll.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin; listScroll.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin;
TCMSSetAccessibility(listScroll, [NSString stringWithFormat:@"%@ list", [section capitalizedString]], @"Select a row to load it into the editor.");
TCMSSetAccessibility(self.tableView, [NSString stringWithFormat:@"%@ table", [section capitalizedString]], @"Select a row to load it into the editor.");
self.previousPageButton = [NSButton buttonWithTitle:@"Previous" target:self action:@selector(previousContentPage)]; self.previousPageButton = [NSButton buttonWithTitle:@"Previous" target:self action:@selector(previousContentPage)];
self.previousPageButton.frame = NSMakeRect(20, 20, 78, 28); self.previousPageButton.frame = NSMakeRect(20, 20, 78, 28);
self.previousPageButton.bezelStyle = NSBezelStyleRounded; self.previousPageButton.bezelStyle = NSBezelStyleRounded;
self.previousPageButton.autoresizingMask = NSViewMaxYMargin; self.previousPageButton.autoresizingMask = NSViewMaxYMargin;
TCMSSetAccessibility(self.previousPageButton, @"Previous page", [NSString stringWithFormat:@"Show the previous page of %@.", section]);
[self.workspace addSubview:self.previousPageButton]; [self.workspace addSubview:self.previousPageButton];
self.pageInfoLabel = [self label:@"1 / 1" frame:NSMakeRect(104, 24, 66, 20) font:[NSFont systemFontOfSize:12]]; self.pageInfoLabel = [self label:@"1 / 1" frame:NSMakeRect(104, 24, 66, 20) font:[NSFont systemFontOfSize:12]];
self.pageInfoLabel.alignment = NSTextAlignmentCenter; self.pageInfoLabel.alignment = NSTextAlignmentCenter;
self.pageInfoLabel.autoresizingMask = NSViewMaxYMargin; self.pageInfoLabel.autoresizingMask = NSViewMaxYMargin;
TCMSSetAccessibility(self.pageInfoLabel, @"Page", @"Current list page.");
TCMSSetAccessibilityValue(self.pageInfoLabel, @"Page 1 of 1");
[self.workspace addSubview:self.pageInfoLabel]; [self.workspace addSubview:self.pageInfoLabel];
self.nextPageButton = [NSButton buttonWithTitle:@"Next" target:self action:@selector(nextContentPage)]; self.nextPageButton = [NSButton buttonWithTitle:@"Next" target:self action:@selector(nextContentPage)];
self.nextPageButton.frame = NSMakeRect(176, 20, 54, 28); self.nextPageButton.frame = NSMakeRect(176, 20, 54, 28);
self.nextPageButton.bezelStyle = NSBezelStyleRounded; self.nextPageButton.bezelStyle = NSBezelStyleRounded;
self.nextPageButton.autoresizingMask = NSViewMaxYMargin; self.nextPageButton.autoresizingMask = NSViewMaxYMargin;
TCMSSetAccessibility(self.nextPageButton, @"Next page", [NSString stringWithFormat:@"Show the next page of %@.", section]);
[self.workspace addSubview:self.nextPageButton]; [self.workspace addSubview:self.nextPageButton];
self.perPagePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(236, 20, 64, 28)]; self.perPagePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(236, 20, 64, 28)];
self.perPagePopup.autoresizingMask = NSViewMaxYMargin; self.perPagePopup.autoresizingMask = NSViewMaxYMargin;
self.perPagePopup.toolTip = @"Items per page"; self.perPagePopup.toolTip = @"Items per page";
TCMSSetAccessibility(self.perPagePopup, @"Items per page", [NSString stringWithFormat:@"Choose how many %@ to show per page.", section]);
self.perPagePopup.target = self; self.perPagePopup.target = self;
self.perPagePopup.action = @selector(contentPerPageChanged:); self.perPagePopup.action = @selector(contentPerPageChanged:);
[self.perPagePopup addItemsWithTitles:@[@"10", @"25", @"50", @"100"]]; [self.perPagePopup addItemsWithTitles:@[@"10", @"25", @"50", @"100"]];
@@ -602,9 +663,11 @@ static NSString *TCMSFormatBytes(id value) {
self.titleField.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; self.titleField.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
self.statusPopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(left + width - 220, 646, 100, 28)]; self.statusPopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(left + width - 220, 646, 100, 28)];
self.statusPopup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin; self.statusPopup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;
TCMSSetAccessibility(self.statusPopup, @"Status", @"Choose draft or published.");
[self.statusPopup addItemsWithTitles:@[@"draft", @"published"]]; [self.statusPopup addItemsWithTitles:@[@"draft", @"published"]];
self.typePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(left + width - 110, 646, 90, 28)]; self.typePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(left + width - 110, 646, 90, 28)];
self.typePopup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin; self.typePopup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;
TCMSSetAccessibility(self.typePopup, @"Content type", @"Choose post or page.");
[self.typePopup addItemsWithTitles:@[@"post", @"page"]]; [self.typePopup addItemsWithTitles:@[@"post", @"page"]];
[self.workspace addSubview:self.titleField]; [self.workspace addSubview:self.titleField];
[self.workspace addSubview:self.statusPopup]; [self.workspace addSubview:self.statusPopup];
@@ -625,12 +688,15 @@ static NSString *TCMSFormatBytes(id value) {
self.allowCommentsButton = [NSButton checkboxWithTitle:@"Allow comments" target:self action:@selector(markDirty)]; self.allowCommentsButton = [NSButton checkboxWithTitle:@"Allow comments" target:self action:@selector(markDirty)];
self.allowCommentsButton.frame = NSMakeRect(left, 460, 180, 24); self.allowCommentsButton.frame = NSMakeRect(left, 460, 180, 24);
self.allowCommentsButton.autoresizingMask = NSViewMinYMargin; self.allowCommentsButton.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(self.allowCommentsButton, @"Allow comments", @"Allow readers to leave comments on this content.");
self.spellCheckButton = [NSButton checkboxWithTitle:@"Spell check" target:self action:@selector(editorOptionChanged:)]; self.spellCheckButton = [NSButton checkboxWithTitle:@"Spell check" target:self action:@selector(editorOptionChanged:)];
self.spellCheckButton.frame = NSMakeRect(left + 190, 460, 120, 24); self.spellCheckButton.frame = NSMakeRect(left + 190, 460, 120, 24);
self.spellCheckButton.autoresizingMask = NSViewMinYMargin; self.spellCheckButton.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(self.spellCheckButton, @"Spell check", @"Toggle continuous spell checking in the Markdown editor.");
self.autoCorrectButton = [NSButton checkboxWithTitle:@"Auto-correct" target:self action:@selector(editorOptionChanged:)]; self.autoCorrectButton = [NSButton checkboxWithTitle:@"Auto-correct" target:self action:@selector(editorOptionChanged:)];
self.autoCorrectButton.frame = NSMakeRect(left + 320, 460, 130, 24); self.autoCorrectButton.frame = NSMakeRect(left + 320, 460, 130, 24);
self.autoCorrectButton.autoresizingMask = NSViewMinYMargin; self.autoCorrectButton.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(self.autoCorrectButton, @"Auto-correct", @"Toggle automatic corrections in the Markdown editor.");
for (NSView *view in @[self.slugField, self.categoryField, self.tagsField, self.summaryField, self.authorField, self.coverField, self.allowCommentsButton, self.spellCheckButton, self.autoCorrectButton]) { for (NSView *view in @[self.slugField, self.categoryField, self.tagsField, self.summaryField, self.authorField, self.coverField, self.allowCommentsButton, self.spellCheckButton, self.autoCorrectButton]) {
[self.workspace addSubview:view]; [self.workspace addSubview:view];
} }
@@ -641,6 +707,7 @@ static NSString *TCMSFormatBytes(id value) {
self.editorScroll.hasHorizontalScroller = NO; self.editorScroll.hasHorizontalScroller = NO;
self.editorScroll.autohidesScrollers = YES; self.editorScroll.autohidesScrollers = YES;
self.editorScroll.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; self.editorScroll.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
TCMSSetAccessibility(self.editorScroll, @"Markdown editor", @"Edit the Markdown body for the current post or page.");
self.markdownView = [[TCMSMarkdownTextView alloc] initWithFrame:self.editorScroll.contentView.bounds]; self.markdownView = [[TCMSMarkdownTextView alloc] initWithFrame:self.editorScroll.contentView.bounds];
self.markdownView.font = [NSFont fontWithName:@"Menlo" size:13] ?: [NSFont monospacedSystemFontOfSize:13 weight:NSFontWeightRegular]; self.markdownView.font = [NSFont fontWithName:@"Menlo" size:13] ?: [NSFont monospacedSystemFontOfSize:13 weight:NSFontWeightRegular];
self.markdownView.allowsUndo = YES; self.markdownView.allowsUndo = YES;
@@ -651,6 +718,7 @@ static NSString *TCMSFormatBytes(id value) {
self.markdownView.textContainer.heightTracksTextView = NO; self.markdownView.textContainer.heightTracksTextView = NO;
self.markdownView.textContainerInset = NSMakeSize(8, 8); self.markdownView.textContainerInset = NSMakeSize(8, 8);
self.markdownView.delegate = self; self.markdownView.delegate = self;
TCMSSetAccessibility(self.markdownView, @"Markdown editor", @"Edit the Markdown body for the current post or page.");
self.editorScroll.documentView = self.markdownView; self.editorScroll.documentView = self.markdownView;
[self.workspace addSubview:self.editorScroll]; [self.workspace addSubview:self.editorScroll];
[self resizeEditorTextView]; [self resizeEditorTextView];
@@ -669,18 +737,27 @@ static NSString *TCMSFormatBytes(id value) {
self.section = @"media"; self.section = @"media";
[self clearWorkspace]; [self clearWorkspace];
self.contentListPage = 1; self.contentListPage = 1;
[self.workspace addSubview:[self label:@"Media" frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]]]; NSTextField *heading = [self label:@"Media" frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]];
[self addButton:@"Refresh" frame:NSMakeRect(210, 690, 78, 30) action:@selector(refreshCurrentSection)]; [self.workspace addSubview:heading];
[self addButton:@"Upload" frame:NSMakeRect(296, 690, 78, 30) action:@selector(uploadMedia)]; [self configureWorkspaceAccessibilityWithTitle:@"Media" heading:heading];
NSButton *refreshButton = [self addButton:@"Refresh" frame:NSMakeRect(210, 690, 78, 30) action:@selector(refreshCurrentSection)];
NSButton *uploadButton = [self addButton:@"Upload" frame:NSMakeRect(296, 690, 78, 30) action:@selector(uploadMedia)];
self.openMediaButton = [self addButton:@"Open" frame:NSMakeRect(382, 690, 66, 30) action:@selector(openSelectedMedia:)]; self.openMediaButton = [self addButton:@"Open" frame:NSMakeRect(382, 690, 66, 30) action:@selector(openSelectedMedia:)];
self.openMediaButton.enabled = NO; self.openMediaButton.enabled = NO;
self.mediaURLCopyButton = [self addButton:@"Copy URL" frame:NSMakeRect(456, 690, 88, 30) action:@selector(copySelectedMediaURL:)]; self.mediaURLCopyButton = [self addButton:@"Copy URL" frame:NSMakeRect(456, 690, 88, 30) action:@selector(copySelectedMediaURL:)];
self.mediaURLCopyButton.enabled = NO; self.mediaURLCopyButton.enabled = NO;
self.mediaEmbedCopyButton = [self addButton:@"Copy Embed" frame:NSMakeRect(552, 690, 104, 30) action:@selector(copySelectedMediaEmbed:)]; self.mediaEmbedCopyButton = [self addButton:@"Copy Embed" frame:NSMakeRect(552, 690, 104, 30) action:@selector(copySelectedMediaEmbed:)];
self.mediaEmbedCopyButton.enabled = NO; self.mediaEmbedCopyButton.enabled = NO;
TCMSSetAccessibility(refreshButton, @"Refresh media list", nil);
TCMSSetAccessibility(uploadButton, @"Upload media", @"Upload a file to the TCMS media library.");
TCMSSetAccessibility(self.openMediaButton, @"Open selected media", @"Open the selected media item in the default app.");
TCMSSetAccessibility(self.mediaURLCopyButton, @"Copy selected media URL", nil);
TCMSSetAccessibility(self.mediaEmbedCopyButton, @"Copy Markdown embed for selected media", nil);
NSScrollView *mediaScroll = [self buildTableWithFrame:NSMakeRect(20, 56, 420, 614) columns:@[@"name", @"mime", @"size"]]; NSScrollView *mediaScroll = [self buildTableWithFrame:NSMakeRect(20, 56, 420, 614) columns:@[@"name", @"mime", @"size"]];
mediaScroll.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin; mediaScroll.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin;
TCMSSetAccessibility(mediaScroll, @"Media list", @"Select a row to preview media.");
TCMSSetAccessibility(self.tableView, @"Media table", @"Select a row to preview media.");
self.tableView.target = self; self.tableView.target = self;
self.tableView.doubleAction = @selector(openSelectedMedia:); self.tableView.doubleAction = @selector(openSelectedMedia:);
[self configureMediaContextMenu]; [self configureMediaContextMenu];
@@ -689,22 +766,27 @@ static NSString *TCMSFormatBytes(id value) {
self.previousPageButton.frame = NSMakeRect(20, 20, 78, 28); self.previousPageButton.frame = NSMakeRect(20, 20, 78, 28);
self.previousPageButton.bezelStyle = NSBezelStyleRounded; self.previousPageButton.bezelStyle = NSBezelStyleRounded;
self.previousPageButton.autoresizingMask = NSViewMaxYMargin; self.previousPageButton.autoresizingMask = NSViewMaxYMargin;
TCMSSetAccessibility(self.previousPageButton, @"Previous media page", nil);
[self.workspace addSubview:self.previousPageButton]; [self.workspace addSubview:self.previousPageButton];
self.pageInfoLabel = [self label:@"1 / 1" frame:NSMakeRect(104, 24, 66, 20) font:[NSFont systemFontOfSize:12]]; self.pageInfoLabel = [self label:@"1 / 1" frame:NSMakeRect(104, 24, 66, 20) font:[NSFont systemFontOfSize:12]];
self.pageInfoLabel.alignment = NSTextAlignmentCenter; self.pageInfoLabel.alignment = NSTextAlignmentCenter;
self.pageInfoLabel.autoresizingMask = NSViewMaxYMargin; self.pageInfoLabel.autoresizingMask = NSViewMaxYMargin;
TCMSSetAccessibility(self.pageInfoLabel, @"Page", @"Current media list page.");
TCMSSetAccessibilityValue(self.pageInfoLabel, @"Page 1 of 1");
[self.workspace addSubview:self.pageInfoLabel]; [self.workspace addSubview:self.pageInfoLabel];
self.nextPageButton = [NSButton buttonWithTitle:@"Next" target:self action:@selector(nextContentPage)]; self.nextPageButton = [NSButton buttonWithTitle:@"Next" target:self action:@selector(nextContentPage)];
self.nextPageButton.frame = NSMakeRect(176, 20, 54, 28); self.nextPageButton.frame = NSMakeRect(176, 20, 54, 28);
self.nextPageButton.bezelStyle = NSBezelStyleRounded; self.nextPageButton.bezelStyle = NSBezelStyleRounded;
self.nextPageButton.autoresizingMask = NSViewMaxYMargin; self.nextPageButton.autoresizingMask = NSViewMaxYMargin;
TCMSSetAccessibility(self.nextPageButton, @"Next media page", nil);
[self.workspace addSubview:self.nextPageButton]; [self.workspace addSubview:self.nextPageButton];
self.perPagePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(236, 20, 64, 28)]; self.perPagePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(236, 20, 64, 28)];
self.perPagePopup.autoresizingMask = NSViewMaxYMargin; self.perPagePopup.autoresizingMask = NSViewMaxYMargin;
self.perPagePopup.toolTip = @"Items per page"; self.perPagePopup.toolTip = @"Items per page";
TCMSSetAccessibility(self.perPagePopup, @"Media items per page", nil);
self.perPagePopup.target = self; self.perPagePopup.target = self;
self.perPagePopup.action = @selector(contentPerPageChanged:); self.perPagePopup.action = @selector(contentPerPageChanged:);
[self.perPagePopup addItemsWithTitles:@[@"10", @"25", @"50", @"100"]]; [self.perPagePopup addItemsWithTitles:@[@"10", @"25", @"50", @"100"]];
@@ -723,16 +805,20 @@ static NSString *TCMSFormatBytes(id value) {
self.mediaPreviewContainer.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; self.mediaPreviewContainer.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
self.mediaPreviewContainer.wantsLayer = YES; self.mediaPreviewContainer.wantsLayer = YES;
self.mediaPreviewContainer.layer.backgroundColor = [NSColor colorWithCalibratedWhite:0.11 alpha:1].CGColor; self.mediaPreviewContainer.layer.backgroundColor = [NSColor colorWithCalibratedWhite:0.11 alpha:1].CGColor;
TCMSSetAccessibility(self.mediaPreviewContainer, @"Media preview", @"Preview for the selected media item.");
[self.workspace addSubview:self.mediaPreviewContainer]; [self.workspace addSubview:self.mediaPreviewContainer];
self.mediaPreviewLabel = [self label:@"No media selected" frame:NSMakeRect(16, 612, previewWidth - 32, 24) font:[NSFont boldSystemFontOfSize:15]]; self.mediaPreviewLabel = [self label:@"No media selected" frame:NSMakeRect(16, 612, previewWidth - 32, 24) font:[NSFont boldSystemFontOfSize:15]];
self.mediaPreviewLabel.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; self.mediaPreviewLabel.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
self.mediaPreviewLabel.textColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1]; self.mediaPreviewLabel.textColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1];
TCMSSetAccessibility(self.mediaPreviewLabel, @"Selected media", nil);
TCMSSetAccessibilityValue(self.mediaPreviewLabel, @"No media selected");
[self.mediaPreviewContainer addSubview:self.mediaPreviewLabel]; [self.mediaPreviewContainer addSubview:self.mediaPreviewLabel];
self.mediaDetailLabel = [self label:@"" frame:NSMakeRect(16, 584, previewWidth - 32, 20) font:[NSFont systemFontOfSize:12]]; self.mediaDetailLabel = [self label:@"" frame:NSMakeRect(16, 584, previewWidth - 32, 20) font:[NSFont systemFontOfSize:12]];
self.mediaDetailLabel.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; self.mediaDetailLabel.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
self.mediaDetailLabel.textColor = [NSColor colorWithCalibratedWhite:0.72 alpha:1]; self.mediaDetailLabel.textColor = [NSColor colorWithCalibratedWhite:0.72 alpha:1];
TCMSSetAccessibility(self.mediaDetailLabel, @"Media details", nil);
[self.mediaPreviewContainer addSubview:self.mediaDetailLabel]; [self.mediaPreviewContainer addSubview:self.mediaDetailLabel];
NSRect previewFrame = NSMakeRect(16, 16, previewWidth - 32, 552); NSRect previewFrame = NSMakeRect(16, 16, previewWidth - 32, 552);
@@ -740,12 +826,14 @@ static NSString *TCMSFormatBytes(id value) {
self.mediaImageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; self.mediaImageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
self.mediaImageView.imageScaling = NSImageScaleProportionallyUpOrDown; self.mediaImageView.imageScaling = NSImageScaleProportionallyUpOrDown;
self.mediaImageView.hidden = YES; self.mediaImageView.hidden = YES;
TCMSSetAccessibility(self.mediaImageView, @"Image preview", @"Preview image for the selected media item.");
[self.mediaPreviewContainer addSubview:self.mediaImageView]; [self.mediaPreviewContainer addSubview:self.mediaImageView];
self.mediaPlayerView = [[AVPlayerView alloc] initWithFrame:previewFrame]; self.mediaPlayerView = [[AVPlayerView alloc] initWithFrame:previewFrame];
self.mediaPlayerView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; self.mediaPlayerView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
self.mediaPlayerView.controlsStyle = AVPlayerViewControlsStyleDefault; self.mediaPlayerView.controlsStyle = AVPlayerViewControlsStyleDefault;
self.mediaPlayerView.hidden = YES; self.mediaPlayerView.hidden = YES;
TCMSSetAccessibility(self.mediaPlayerView, @"Media player", @"Play the selected audio or video item.");
[self.mediaPreviewContainer addSubview:self.mediaPlayerView]; [self.mediaPreviewContainer addSubview:self.mediaPlayerView];
[self resetMediaPreview]; [self resetMediaPreview];
@@ -756,18 +844,28 @@ static NSString *TCMSFormatBytes(id value) {
- (void)showCommentsSection { - (void)showCommentsSection {
self.section = @"comments"; self.section = @"comments";
[self clearWorkspace]; [self clearWorkspace];
[self.workspace addSubview:[self label:@"Comments" frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]]]; NSTextField *heading = [self label:@"Comments" frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]];
[self.workspace addSubview:heading];
[self configureWorkspaceAccessibilityWithTitle:@"Comments" heading:heading];
self.commentStatusPopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(210, 690, 120, 30)]; self.commentStatusPopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(210, 690, 120, 30)];
self.commentStatusPopup.autoresizingMask = NSViewMinYMargin; self.commentStatusPopup.autoresizingMask = NSViewMinYMargin;
[self.commentStatusPopup addItemsWithTitles:@[@"pending", @"approved", @"spam", @"all"]]; [self.commentStatusPopup addItemsWithTitles:@[@"pending", @"approved", @"spam", @"all"]];
TCMSSetAccessibility(self.commentStatusPopup, @"Comment status filter", @"Choose which comments to list.");
[self.workspace addSubview:self.commentStatusPopup]; [self.workspace addSubview:self.commentStatusPopup];
[self addButton:@"Refresh" frame:NSMakeRect(340, 690, 90, 30) action:@selector(refreshCurrentSection)]; NSButton *refreshButton = [self addButton:@"Refresh" frame:NSMakeRect(340, 690, 90, 30) action:@selector(refreshCurrentSection)];
[self addButton:@"Approve" frame:NSMakeRect(440, 690, 90, 30) action:@selector(approveComment)]; NSButton *approveButton = [self addButton:@"Approve" frame:NSMakeRect(440, 690, 90, 30) action:@selector(approveComment)];
[self addButton:@"Pending" frame:NSMakeRect(540, 690, 90, 30) action:@selector(pendingComment)]; NSButton *pendingButton = [self addButton:@"Pending" frame:NSMakeRect(540, 690, 90, 30) action:@selector(pendingComment)];
[self addButton:@"Spam" frame:NSMakeRect(640, 690, 80, 30) action:@selector(spamComment)]; NSButton *spamButton = [self addButton:@"Spam" frame:NSMakeRect(640, 690, 80, 30) action:@selector(spamComment)];
[self addButton:@"Delete" frame:NSMakeRect(730, 690, 80, 30) action:@selector(deleteComment)]; NSButton *deleteButton = [self addButton:@"Delete" frame:NSMakeRect(730, 690, 80, 30) action:@selector(deleteComment)];
TCMSSetAccessibility(refreshButton, @"Refresh comments", nil);
TCMSSetAccessibility(approveButton, @"Approve selected comment", nil);
TCMSSetAccessibility(pendingButton, @"Mark selected comment pending", nil);
TCMSSetAccessibility(spamButton, @"Mark selected comment as spam", nil);
TCMSSetAccessibility(deleteButton, @"Delete selected comment", @"Deletes the selected comment from the server.");
NSScrollView *commentsScroll = [self buildTableWithFrame:NSMakeRect(20, 20, self.workspace.bounds.size.width - 40, 650) columns:@[@"author", @"status", @"slug", @"body"]]; NSScrollView *commentsScroll = [self buildTableWithFrame:NSMakeRect(20, 20, self.workspace.bounds.size.width - 40, 650) columns:@[@"author", @"status", @"slug", @"body"]];
commentsScroll.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; commentsScroll.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
TCMSSetAccessibility(commentsScroll, @"Comments list", @"Select a comment row before moderating it.");
TCMSSetAccessibility(self.tableView, @"Comments table", @"Select a comment row before moderating it.");
[self refreshComments]; [self refreshComments];
[self refreshWindowLayout]; [self refreshWindowLayout];
} }
@@ -775,7 +873,9 @@ static NSString *TCMSFormatBytes(id value) {
- (void)showSettingsSection { - (void)showSettingsSection {
self.section = @"settings"; self.section = @"settings";
[self clearWorkspace]; [self clearWorkspace];
[self.workspace addSubview:[self label:@"Settings" frame:NSMakeRect(20, 690, 220, 28) font:[NSFont boldSystemFontOfSize:24]]]; NSTextField *heading = [self label:@"Settings" frame:NSMakeRect(20, 690, 220, 28) font:[NSFont boldSystemFontOfSize:24]];
[self.workspace addSubview:heading];
[self configureWorkspaceAccessibilityWithTitle:@"Settings" heading:heading];
[self.workspace addSubview:[self label:@"TCMS URL" frame:NSMakeRect(20, 640, 140, 24) font:[NSFont systemFontOfSize:13]]]; [self.workspace addSubview:[self label:@"TCMS URL" frame:NSMakeRect(20, 640, 140, 24) font:[NSFont systemFontOfSize:13]]];
[self.workspace addSubview:[self label:@"Username" frame:NSMakeRect(20, 596, 140, 24) font:[NSFont systemFontOfSize:13]]]; [self.workspace addSubview:[self label:@"Username" frame:NSMakeRect(20, 596, 140, 24) font:[NSFont systemFontOfSize:13]]];
[self.workspace addSubview:[self label:@"Password" frame:NSMakeRect(20, 552, 140, 24) font:[NSFont systemFontOfSize:13]]]; [self.workspace addSubview:[self label:@"Password" frame:NSMakeRect(20, 552, 140, 24) font:[NSFont systemFontOfSize:13]]];
@@ -783,22 +883,29 @@ static NSString *TCMSFormatBytes(id value) {
self.siteField = [self field:@"https://example.com/blog" frame:NSMakeRect(170, 638, 520, 28)]; self.siteField = [self field:@"https://example.com/blog" frame:NSMakeRect(170, 638, 520, 28)];
self.siteField.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; self.siteField.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
TCMSSetAccessibility(self.siteField, @"TCMS URL", @"Base URL for the TCMS site.");
self.usernameField = [self field:@"admin" frame:NSMakeRect(170, 594, 260, 28)]; self.usernameField = [self field:@"admin" frame:NSMakeRect(170, 594, 260, 28)];
self.usernameField.autoresizingMask = NSViewMinYMargin; self.usernameField.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(self.usernameField, @"Username", @"Username for the TCMS API.");
self.passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(170, 550, 260, 28)]; self.passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(170, 550, 260, 28)];
self.passwordField.delegate = self; self.passwordField.delegate = self;
self.passwordField.autoresizingMask = NSViewMinYMargin; self.passwordField.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(self.passwordField, @"Password", @"Password for the TCMS API.");
self.autosaveField = [self field:@"30" frame:NSMakeRect(170, 506, 120, 28)]; self.autosaveField = [self field:@"30" frame:NSMakeRect(170, 506, 120, 28)];
TCMSSetAccessibility(self.autosaveField, @"Autosave seconds", @"Minimum autosave interval is 10 seconds.");
self.autosaveField.autoresizingMask = NSViewMinYMargin; self.autosaveField.autoresizingMask = NSViewMinYMargin;
self.remoteAutosaveButton = [NSButton checkboxWithTitle:@"Also save remotely as draft" target:nil action:nil]; self.remoteAutosaveButton = [NSButton checkboxWithTitle:@"Also save remotely as draft" target:nil action:nil];
self.remoteAutosaveButton.frame = NSMakeRect(170, 464, 260, 24); self.remoteAutosaveButton.frame = NSMakeRect(170, 464, 260, 24);
self.remoteAutosaveButton.autoresizingMask = NSViewMinYMargin; self.remoteAutosaveButton.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(self.remoteAutosaveButton, @"Also save remotely as draft", @"When enabled, autosave sends a draft copy to the server.");
for (NSView *view in @[self.siteField, self.usernameField, self.passwordField, self.autosaveField, self.remoteAutosaveButton]) { for (NSView *view in @[self.siteField, self.usernameField, self.passwordField, self.autosaveField, self.remoteAutosaveButton]) {
[self.workspace addSubview:view]; [self.workspace addSubview:view];
} }
[self addButton:@"Save Settings" frame:NSMakeRect(170, 414, 120, 32) action:@selector(saveSettings)]; NSButton *saveButton = [self addButton:@"Save Settings" frame:NSMakeRect(170, 414, 120, 32) action:@selector(saveSettings)];
[self addButton:@"Test Auth" frame:NSMakeRect(300, 414, 100, 32) action:@selector(testConnection)]; NSButton *testButton = [self addButton:@"Test Auth" frame:NSMakeRect(300, 414, 100, 32) action:@selector(testConnection)];
TCMSSetAccessibility(saveButton, @"Save settings", nil);
TCMSSetAccessibility(testButton, @"Test authentication", @"Save settings, then test the TCMS API credentials.");
[self populateSettingsFields]; [self populateSettingsFields];
[self wireSettingsKeyLoop]; [self wireSettingsKeyLoop];
[self refreshWindowLayout]; [self refreshWindowLayout];
@@ -809,6 +916,7 @@ static NSString *TCMSFormatBytes(id value) {
button.frame = frame; button.frame = frame;
button.bezelStyle = NSBezelStyleRounded; button.bezelStyle = NSBezelStyleRounded;
button.autoresizingMask = NSViewMinYMargin; button.autoresizingMask = NSViewMinYMargin;
TCMSSetAccessibility(button, title, nil);
[self.workspace addSubview:button]; [self.workspace addSubview:button];
return button; return button;
} }
@@ -821,6 +929,8 @@ static NSString *TCMSFormatBytes(id value) {
self.tableView.delegate = self; self.tableView.delegate = self;
self.tableView.dataSource = self; self.tableView.dataSource = self;
self.tableView.columnAutoresizingStyle = NSTableViewUniformColumnAutoresizingStyle; self.tableView.columnAutoresizingStyle = NSTableViewUniformColumnAutoresizingStyle;
TCMSSetAccessibility(scroll, @"Table", @"Use arrow keys to move through rows.");
TCMSSetAccessibility(self.tableView, @"Table", @"Use arrow keys to move through rows.");
for (NSString *identifier in columns) { for (NSString *identifier in columns) {
NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:identifier]; NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:identifier];
column.title = [identifier capitalizedString]; column.title = [identifier capitalizedString];
@@ -854,13 +964,16 @@ static NSString *TCMSFormatBytes(id value) {
[self.tableView deselectAll:nil]; [self.tableView deselectAll:nil];
[self.tableView reloadData]; [self.tableView reloadData];
self.pageInfoLabel.stringValue = total == 0 ? @"0 / 0" : [NSString stringWithFormat:@"%ld / %ld", (long)self.contentListPage, (long)totalPages]; NSString *pageText = total == 0 ? @"0 / 0" : [NSString stringWithFormat:@"%ld / %ld", (long)self.contentListPage, (long)totalPages];
self.pageInfoLabel.stringValue = pageText;
TCMSSetAccessibilityValue(self.pageInfoLabel, total == 0 ? @"No pages" : [NSString stringWithFormat:@"Page %ld of %ld", (long)self.contentListPage, (long)totalPages]);
self.previousPageButton.enabled = total > 0 && self.contentListPage > 1; self.previousPageButton.enabled = total > 0 && self.contentListPage > 1;
self.nextPageButton.enabled = total > 0 && self.contentListPage < totalPages; self.nextPageButton.enabled = total > 0 && self.contentListPage < totalPages;
self.perPagePopup.enabled = total > 0; self.perPagePopup.enabled = total > 0;
if ([self.section isEqualToString:@"media"]) { if ([self.section isEqualToString:@"media"]) {
[self resetMediaPreview]; [self resetMediaPreview];
} }
TCMSSetAccessibilityValue(self.tableView, [self currentListStatusMessage]);
[self refreshWindowLayout]; [self refreshWindowLayout];
} }
@@ -928,6 +1041,7 @@ static NSString *TCMSFormatBytes(id value) {
} }
NSDictionary *item = self.tableItems[row]; NSDictionary *item = self.tableItems[row];
field.stringValue = [tableColumn.identifier isEqualToString:@"size"] ? TCMSFormatBytes(item[@"size"]) : TCMSString(item[tableColumn.identifier]); field.stringValue = [tableColumn.identifier isEqualToString:@"size"] ? TCMSFormatBytes(item[@"size"]) : TCMSString(item[tableColumn.identifier]);
TCMSSetAccessibility(field, [NSString stringWithFormat:@"%@: %@", tableColumn.title ?: TCMSString(tableColumn.identifier), field.stringValue], nil);
return field; return field;
} }
@@ -1570,6 +1684,9 @@ static NSString *TCMSFormatBytes(id value) {
self.mediaEmbedCopyButton.enabled = NO; self.mediaEmbedCopyButton.enabled = NO;
self.mediaPreviewLabel.stringValue = @"No media selected"; self.mediaPreviewLabel.stringValue = @"No media selected";
self.mediaDetailLabel.stringValue = @""; self.mediaDetailLabel.stringValue = @"";
TCMSSetAccessibilityValue(self.mediaPreviewContainer, @"No media selected");
TCMSSetAccessibilityValue(self.mediaPreviewLabel, @"No media selected");
TCMSSetAccessibilityValue(self.mediaDetailLabel, @"");
} }
- (void)previewMediaItem:(NSDictionary *)item { - (void)previewMediaItem:(NSDictionary *)item {
@@ -1586,8 +1703,12 @@ static NSString *TCMSFormatBytes(id value) {
self.openMediaButton.enabled = YES; self.openMediaButton.enabled = YES;
self.mediaURLCopyButton.enabled = YES; self.mediaURLCopyButton.enabled = YES;
self.mediaEmbedCopyButton.enabled = YES; self.mediaEmbedCopyButton.enabled = YES;
self.mediaPreviewLabel.stringValue = TCMSString(item[@"name"]).length ? TCMSString(item[@"name"]) : url.lastPathComponent; NSString *mediaName = TCMSString(item[@"name"]).length ? TCMSString(item[@"name"]) : url.lastPathComponent;
self.mediaPreviewLabel.stringValue = mediaName;
self.mediaDetailLabel.stringValue = [self mediaDetailForItem:item]; self.mediaDetailLabel.stringValue = [self mediaDetailForItem:item];
TCMSSetAccessibilityValue(self.mediaPreviewContainer, [NSString stringWithFormat:@"%@. %@", mediaName, self.mediaDetailLabel.stringValue]);
TCMSSetAccessibilityValue(self.mediaPreviewLabel, mediaName);
TCMSSetAccessibilityValue(self.mediaDetailLabel, self.mediaDetailLabel.stringValue);
self.mediaImageView.image = nil; self.mediaImageView.image = nil;
self.mediaImageView.hidden = YES; self.mediaImageView.hidden = YES;
[self.mediaPlayerView.player pause]; [self.mediaPlayerView.player pause];
@@ -1596,6 +1717,7 @@ static NSString *TCMSFormatBytes(id value) {
if ([mime hasPrefix:@"image/"]) { if ([mime hasPrefix:@"image/"]) {
self.mediaImageView.hidden = NO; self.mediaImageView.hidden = NO;
TCMSSetAccessibility(self.mediaImageView, [NSString stringWithFormat:@"Image preview for %@", mediaName], nil);
[self setStatus:@"Loading image preview."]; [self setStatus:@"Loading image preview."];
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSImage *image = data.length > 0 ? [[NSImage alloc] initWithData:data] : nil; NSImage *image = data.length > 0 ? [[NSImage alloc] initWithData:data] : nil;
@@ -1619,6 +1741,7 @@ static NSString *TCMSFormatBytes(id value) {
AVPlayer *player = [AVPlayer playerWithURL:url]; AVPlayer *player = [AVPlayer playerWithURL:url];
self.mediaPlayerView.player = player; self.mediaPlayerView.player = player;
self.mediaPlayerView.hidden = NO; self.mediaPlayerView.hidden = NO;
TCMSSetAccessibility(self.mediaPlayerView, [mime hasPrefix:@"audio/"] ? [NSString stringWithFormat:@"Audio player for %@", mediaName] : [NSString stringWithFormat:@"Video player for %@", mediaName], nil);
[self setStatus:[mime hasPrefix:@"audio/"] ? @"Audio ready to play." : @"Video ready to play."]; [self setStatus:[mime hasPrefix:@"audio/"] ? @"Audio ready to play." : @"Video ready to play."];
return; return;
} }
@@ -1652,7 +1775,10 @@ static NSString *TCMSFormatBytes(id value) {
} }
self.tableItems = [NSMutableArray arrayWithArray:json[@"items"] ?: @[]]; self.tableItems = [NSMutableArray arrayWithArray:json[@"items"] ?: @[]];
[self.tableView reloadData]; [self.tableView reloadData];
[self setStatus:@"Comments refreshed."]; NSString *status = self.commentStatusPopup.titleOfSelectedItem ?: @"pending";
NSString *message = [NSString stringWithFormat:@"Showing %ld %@ comments.", (long)self.tableItems.count, status];
TCMSSetAccessibilityValue(self.tableView, message);
[self setStatus:message];
}]; }];
} }
@@ -1748,6 +1874,8 @@ static NSString *TCMSFormatBytes(id value) {
- (void)setStatus:(NSString *)message { - (void)setStatus:(NSString *)message {
self.statusLabel.stringValue = message ?: @""; self.statusLabel.stringValue = message ?: @"";
TCMSSetAccessibilityValue(self.statusLabel, message ?: @"");
[self announceAccessibilityMessage:message ?: @""];
} }
@end @end
Binary file not shown.
+66 -10
View File
@@ -3,19 +3,33 @@
var storedMode = localStorage.getItem('neon-mode') || 'dark'; var storedMode = localStorage.getItem('neon-mode') || 'dark';
var storedAccent = localStorage.getItem('neon-accent') || 'green'; var storedAccent = localStorage.getItem('neon-accent') || 'green';
var modeButton = document.querySelector('[data-mode-toggle]'); var modeButton = document.querySelector('[data-mode-toggle]');
var accentButtons = document.querySelectorAll('[data-accent]');
function labelAccent(accent) {
return accent.charAt(0).toUpperCase() + accent.slice(1);
}
function applyMode(mode) { function applyMode(mode) {
root.dataset.mode = mode; root.dataset.mode = mode;
localStorage.setItem('neon-mode', mode); localStorage.setItem('neon-mode', mode);
if (modeButton) { if (modeButton) {
modeButton.textContent = mode === 'light' ? 'Dark' : 'Light'; modeButton.textContent = 'Light';
modeButton.setAttribute('aria-label', mode === 'light' ? 'Use dark mode' : 'Use light mode'); modeButton.setAttribute('aria-label', 'Light mode');
modeButton.setAttribute('aria-pressed', mode === 'light' ? 'true' : 'false');
modeButton.title = mode === 'light' ? 'Disable light mode' : 'Enable light mode';
} }
} }
function applyAccent(accent) { function applyAccent(accent) {
root.dataset.accent = accent; root.dataset.accent = accent;
localStorage.setItem('neon-accent', accent); localStorage.setItem('neon-accent', accent);
accentButtons.forEach(function (button) {
var selected = button.dataset.accent === accent;
var name = labelAccent(button.dataset.accent || 'accent');
button.setAttribute('aria-pressed', selected ? 'true' : 'false');
button.setAttribute('aria-label', selected ? name + ' accent selected' : 'Use ' + name.toLowerCase() + ' accent');
button.title = selected ? name + ' accent selected' : 'Use ' + name.toLowerCase() + ' accent';
});
} }
applyMode(storedMode); applyMode(storedMode);
@@ -27,7 +41,7 @@
}); });
} }
document.querySelectorAll('[data-accent]').forEach(function (button) { accentButtons.forEach(function (button) {
button.addEventListener('click', function () { button.addEventListener('click', function () {
applyAccent(button.dataset.accent || 'green'); applyAccent(button.dataset.accent || 'green');
}); });
@@ -145,14 +159,15 @@
lightbox.hidden = true; lightbox.hidden = true;
lightbox.setAttribute('role', 'dialog'); lightbox.setAttribute('role', 'dialog');
lightbox.setAttribute('aria-modal', 'true'); lightbox.setAttribute('aria-modal', 'true');
lightbox.setAttribute('aria-label', 'Post media slideshow'); lightbox.setAttribute('aria-labelledby', 'tcms-lightbox-title');
lightbox.setAttribute('aria-describedby', 'tcms-lightbox-caption');
lightbox.innerHTML = [ lightbox.innerHTML = [
'<button class="tcms-lightbox__backdrop" type="button" data-lightbox-close aria-label="Close media viewer"></button>', '<button class="tcms-lightbox__backdrop" type="button" data-lightbox-close aria-label="Close media viewer"></button>',
'<section class="tcms-lightbox__dialog" role="document">', '<section class="tcms-lightbox__dialog" role="document">',
'<header class="tcms-lightbox__bar">', '<header class="tcms-lightbox__bar">',
'<strong class="tcms-lightbox__title"></strong>', '<strong class="tcms-lightbox__title" id="tcms-lightbox-title"></strong>',
'<span class="tcms-lightbox__count"></span>', '<span class="tcms-lightbox__count" aria-live="polite"></span>',
'<button class="tcms-lightbox__close" type="button" data-lightbox-close aria-label="Close">Close</button>', '<button class="tcms-lightbox__close" type="button" data-lightbox-close aria-label="Close">Close</button>',
'</header>', '</header>',
'<div class="tcms-lightbox__stage">', '<div class="tcms-lightbox__stage">',
@@ -160,7 +175,7 @@
'<div class="tcms-lightbox__media"></div>', '<div class="tcms-lightbox__media"></div>',
'<button class="tcms-lightbox__nav tcms-lightbox__nav--next" type="button" data-lightbox-next aria-label="Next media">Next</button>', '<button class="tcms-lightbox__nav tcms-lightbox__nav--next" type="button" data-lightbox-next aria-label="Next media">Next</button>',
'</div>', '</div>',
'<p class="tcms-lightbox__caption"></p>', '<p class="tcms-lightbox__caption" id="tcms-lightbox-caption"></p>',
'</section>' '</section>'
].join(''); ].join('');
@@ -198,6 +213,7 @@
mediaFrame.textContent = ''; mediaFrame.textContent = '';
titleNode.textContent = entry.title; titleNode.textContent = entry.title;
countNode.textContent = (currentIndex + 1) + ' of ' + entries.length; countNode.textContent = (currentIndex + 1) + ' of ' + entries.length;
countNode.setAttribute('aria-label', 'Media ' + (currentIndex + 1) + ' of ' + entries.length);
captionNode.textContent = entry.title; captionNode.textContent = entry.title;
if (entry.type === 'image') { if (entry.type === 'image') {
@@ -219,6 +235,7 @@
video.controls = true; video.controls = true;
video.playsInline = true; video.playsInline = true;
video.preload = 'metadata'; video.preload = 'metadata';
video.setAttribute('aria-label', entry.title);
if (entry.poster) { if (entry.poster) {
video.poster = entry.poster; video.poster = entry.poster;
} }
@@ -253,11 +270,47 @@
lightbox.hidden = true; lightbox.hidden = true;
mediaFrame.textContent = ''; mediaFrame.textContent = '';
document.body.classList.remove('tcms-lightbox-open'); document.body.classList.remove('tcms-lightbox-open');
if (lastFocus && typeof lastFocus.focus === 'function') { if (lastFocus && document.contains(lastFocus) && typeof lastFocus.focus === 'function') {
lastFocus.focus(); lastFocus.focus();
} }
} }
function trapLightboxFocus(event) {
var focusable;
var first;
var last;
if (!lightbox || lightbox.hidden || event.key !== 'Tab') {
return;
}
focusable = Array.prototype.slice.call(lightbox.querySelectorAll([
'button:not([disabled])',
'a[href]',
'iframe',
'video[controls]',
'[tabindex]:not([tabindex="-1"])'
].join(','))).filter(function (element) {
return !element.hidden && element.offsetParent !== null;
});
if (!focusable.length) {
event.preventDefault();
closeButton.focus();
return;
}
first = focusable[0];
last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
function openFromEvent(event, index) { function openFromEvent(event, index) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@@ -277,8 +330,9 @@
} }
if (tag === 'img') { if (tag === 'img') {
trigger.setAttribute('role', 'button'); trigger.setAttribute('role', 'button');
trigger.setAttribute('aria-label', 'Open image viewer'); trigger.setAttribute('aria-label', 'Open image viewer for ' + entry.title);
} }
trigger.setAttribute('aria-haspopup', 'dialog');
trigger.addEventListener('click', function (event) { trigger.addEventListener('click', function (event) {
openFromEvent(event, index); openFromEvent(event, index);
}); });
@@ -294,7 +348,8 @@
button.className = 'tcms-lightbox-trigger'; button.className = 'tcms-lightbox-trigger';
button.type = 'button'; button.type = 'button';
button.textContent = 'View'; button.textContent = 'View';
button.setAttribute('aria-label', 'Open media viewer'); button.setAttribute('aria-haspopup', 'dialog');
button.setAttribute('aria-label', 'Open media viewer for ' + entry.title);
button.addEventListener('click', function (event) { button.addEventListener('click', function (event) {
openFromEvent(event, index); openFromEvent(event, index);
}); });
@@ -319,6 +374,7 @@
} else if (event.key === 'ArrowLeft') { } else if (event.key === 'ArrowLeft') {
showLightboxItem(currentIndex - 1); showLightboxItem(currentIndex - 1);
} }
trapLightboxFocus(event);
}); });
} }
+46
View File
@@ -39,6 +39,15 @@
box-sizing: border-box; box-sizing: border-box;
} }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
html { html {
min-height: 100%; min-height: 100%;
} }
@@ -67,6 +76,15 @@ a:hover {
color: var(--accent); color: var(--accent);
} }
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
[tabindex="-1"]:focus-visible {
outline: 3px solid color-mix(in srgb, var(--accent) 72%, white);
outline-offset: 3px;
}
button, button,
input, input,
textarea { textarea {
@@ -94,6 +112,26 @@ button:hover {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
} }
.skip-link {
position: fixed;
top: 0.75rem;
left: 0.75rem;
z-index: 2000;
transform: translateY(-160%);
border: 1px solid color-mix(in srgb, var(--accent) 72%, white);
border-radius: var(--radius);
padding: 0.55rem 0.75rem;
color: #06100b;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
font-weight: 800;
text-decoration: none;
box-shadow: var(--shadow);
}
.skip-link:focus {
transform: translateY(0);
}
input, input,
textarea { textarea {
width: 100%; width: 100%;
@@ -221,6 +259,14 @@ textarea {
.swatch--purple { background: #a084ff; } .swatch--purple { background: #a084ff; }
.swatch--orange { background: #ff9f43; } .swatch--orange { background: #ff9f43; }
.mode-toggle[aria-pressed="true"],
.swatch[aria-pressed="true"] {
border-color: color-mix(in srgb, var(--accent) 80%, white);
box-shadow:
0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent),
0 0 18px color-mix(in srgb, var(--accent) 34%, transparent);
}
.site-shell { .site-shell {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) 280px; grid-template-columns: minmax(0, 1fr) 280px;
+34 -31
View File
@@ -11,13 +11,13 @@ $description = $item['summary'] ?? $site['description'] ?? '';
$postCard = static function (array $entry): void { ?> $postCard = static function (array $entry): void { ?>
<article class="post-card"> <article class="post-card">
<div class="post-card__meta"> <div class="post-card__meta">
<a href="<?= View::url('category/' . ContentRepository::slugify((string) $entry['category'])) ?>"><?= View::e($entry['category']) ?></a> <a href="<?= View::url('category/' . ContentRepository::slugify((string) $entry['category'])) ?>" aria-label="Category: <?= View::e($entry['category']) ?>"><?= View::e($entry['category']) ?></a>
<span><?= View::date((string) $entry['date']) ?></span> <time datetime="<?= View::e($entry['date']) ?>"><?= View::date((string) $entry['date']) ?></time>
</div> </div>
<h2><a href="<?= View::itemUrl($entry) ?>"><?= View::e($entry['title']) ?></a></h2> <h2><a href="<?= View::itemUrl($entry) ?>"><?= View::e($entry['title']) ?></a></h2>
<p><?= View::e($entry['summary']) ?></p> <p><?= View::e($entry['summary']) ?></p>
<?php if (!empty($entry['tags'])): ?> <?php if (!empty($entry['tags'])): ?>
<div class="tag-row"> <div class="tag-row" aria-label="Tags">
<?php foreach ($entry['tags'] as $tag): ?> <?php foreach ($entry['tags'] as $tag): ?>
<a href="<?= View::url('tag/' . ContentRepository::slugify((string) $tag)) ?>"><?= View::e($tag) ?></a> <a href="<?= View::url('tag/' . ContentRepository::slugify((string) $tag)) ?>"><?= View::e($tag) ?></a>
<?php endforeach; ?> <?php endforeach; ?>
@@ -35,9 +35,9 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
<?php endif; ?> <?php endif; ?>
<?php foreach (($pagination['numbers'] ?? []) as $number): ?> <?php foreach (($pagination['numbers'] ?? []) as $number): ?>
<?php if ((int) $number === (int) $pagination['page']): ?> <?php if ((int) $number === (int) $pagination['page']): ?>
<span class="pager pager--current" aria-current="page"><?= (int) $number ?></span> <span class="pager pager--current" aria-current="page" aria-label="Page <?= (int) $number ?>"><?= (int) $number ?></span>
<?php else: ?> <?php else: ?>
<a class="pager pager--number" href="<?= $url((int) $number) ?>"><?= (int) $number ?></a> <a class="pager pager--number" href="<?= $url((int) $number) ?>" aria-label="Go to page <?= (int) $number ?>"><?= (int) $number ?></a>
<?php endif; ?> <?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
<?php if ($pagination['has_next']): ?> <?php if ($pagination['has_next']): ?>
@@ -61,9 +61,10 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
<script defer src="<?= View::asset('app.js') ?>"></script> <script defer src="<?= View::asset('app.js') ?>"></script>
</head> </head>
<body> <body>
<a class="skip-link" href="#content">Skip to content</a>
<header class="site-header"> <header class="site-header">
<a class="brand" href="<?= View::url('') ?>"> <a class="brand" href="<?= View::url('') ?>">
<span class="brand__mark"></span> <span class="brand__mark" aria-hidden="true"></span>
<span> <span>
<strong><?= View::e($site['title'] ?? "Ty Clifford's Content Management System") ?></strong> <strong><?= View::e($site['title'] ?? "Ty Clifford's Content Management System") ?></strong>
<small><?= View::e($site['tagline'] ?? '') ?></small> <small><?= View::e($site['tagline'] ?? '') ?></small>
@@ -71,40 +72,42 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
</a> </a>
<nav class="main-nav" aria-label="Primary"> <nav class="main-nav" aria-label="Primary">
<a href="<?= View::url('') ?>">Home</a> <a href="<?= View::url('') ?>"<?php if (($routeType ?? '') === 'home'): ?> aria-current="page"<?php endif; ?>>Home</a>
<?php foreach ($navPages as $pageItem): ?> <?php foreach ($navPages as $pageItem): ?>
<a href="<?= View::itemUrl($pageItem) ?>"><?= View::e($pageItem['title']) ?></a> <a href="<?= View::itemUrl($pageItem) ?>"<?php if (($slug ?? '') === ($pageItem['slug'] ?? null)): ?> aria-current="page"<?php endif; ?>><?= View::e($pageItem['title']) ?></a>
<?php endforeach; ?> <?php endforeach; ?>
<a href="<?= View::url('feed.xml') ?>">RSS</a> <a href="<?= View::url('feed.xml') ?>">RSS</a>
</nav> </nav>
<form class="search-form" action="<?= View::url('search') ?>" method="get"> <form class="search-form" action="<?= View::url('search') ?>" method="get" role="search" aria-label="Site search">
<input type="search" name="q" value="<?= View::e($search_query ?? '') ?>" placeholder="Search" aria-label="Search"> <label class="sr-only" for="site-search">Search posts and pages</label>
<input id="site-search" type="search" name="q" value="<?= View::e($search_query ?? '') ?>" placeholder="Search" autocomplete="off">
<button type="submit" aria-label="Search">Go</button> <button type="submit" aria-label="Search">Go</button>
</form> </form>
<div class="theme-tools" aria-label="Theme controls"> <div class="theme-tools" role="group" aria-label="Theme controls">
<button class="mode-toggle" type="button" data-mode-toggle aria-label="Toggle light mode">Light</button> <button class="mode-toggle" type="button" data-mode-toggle aria-pressed="false">Light</button>
<?php foreach (['green', 'blue', 'red', 'pink', 'purple', 'orange'] as $accent): ?> <?php foreach (['green', 'blue', 'red', 'pink', 'purple', 'orange'] as $accent): ?>
<button class="swatch swatch--<?= $accent ?>" type="button" data-accent="<?= $accent ?>" aria-label="<?= ucfirst($accent) ?> accent"></button> <button class="swatch swatch--<?= $accent ?>" type="button" data-accent="<?= $accent ?>" aria-pressed="false" aria-label="Use <?= $accent ?> accent"></button>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</header> </header>
<main class="site-shell"> <main class="site-shell" id="content" tabindex="-1">
<section class="content-lane"> <section class="content-lane">
<?php if ($flash): ?> <?php if ($flash): ?>
<div class="notice notice--<?= View::e($flash['type']) ?>"><?= View::e($flash['message']) ?></div> <div class="notice notice--<?= View::e($flash['type']) ?>" role="<?= ($flash['type'] ?? '') === 'error' ? 'alert' : 'status' ?>" aria-live="<?= ($flash['type'] ?? '') === 'error' ? 'assertive' : 'polite' ?>"><?= View::e($flash['message']) ?></div>
<?php endif; ?> <?php endif; ?>
<?php if ($template === 'listing'): ?> <?php if ($template === 'listing'): ?>
<header class="page-heading"> <header class="page-heading">
<p><?= View::e($site['description'] ?? '') ?></p> <p><?= View::e($site['description'] ?? '') ?></p>
<h1><?= View::e($heading ?? $title ?? '') ?></h1> <h1 id="page-title"><?= View::e($heading ?? $title ?? '') ?></h1>
</header> </header>
<?php if (($search_query ?? null) !== null): ?> <?php if (($search_query ?? null) !== null): ?>
<form class="wide-search" action="<?= View::url('search') ?>" method="get"> <form class="wide-search" action="<?= View::url('search') ?>" method="get" role="search" aria-label="Search archive">
<input type="search" name="q" value="<?= View::e($search_query) ?>" placeholder="Search posts and pages" aria-label="Search posts and pages"> <label class="sr-only" for="archive-search">Search posts and pages</label>
<input id="archive-search" type="search" name="q" value="<?= View::e($search_query) ?>" placeholder="Search posts and pages" autocomplete="off">
<button type="submit">Search</button> <button type="submit">Search</button>
</form> </form>
<?php endif; ?> <?php endif; ?>
@@ -114,7 +117,7 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php if (empty($items)): ?> <?php if (empty($items)): ?>
<p class="empty-state"><?= View::e($empty_message ?? 'No posts found.') ?></p> <p class="empty-state" role="status"><?= View::e($empty_message ?? 'No posts found.') ?></p>
<?php endif; ?> <?php endif; ?>
<?php $renderPagination($pagination, $pagination_url); ?> <?php $renderPagination($pagination, $pagination_url); ?>
<?php elseif ($template === 'single'): ?> <?php elseif ($template === 'single'): ?>
@@ -122,12 +125,12 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
<header class="article__header"> <header class="article__header">
<?php if ($item['type'] === 'post'): ?> <?php if ($item['type'] === 'post'): ?>
<div class="post-card__meta"> <div class="post-card__meta">
<a href="<?= View::url('category/' . ContentRepository::slugify((string) $item['category'])) ?>"><?= View::e($item['category']) ?></a> <a href="<?= View::url('category/' . ContentRepository::slugify((string) $item['category'])) ?>" aria-label="Category: <?= View::e($item['category']) ?>"><?= View::e($item['category']) ?></a>
<span><?= View::date((string) $item['date']) ?></span> <time datetime="<?= View::e($item['date']) ?>"><?= View::date((string) $item['date']) ?></time>
<span><?= View::e($item['author']) ?></span> <span><?= View::e($item['author']) ?></span>
</div> </div>
<?php endif; ?> <?php endif; ?>
<h1><?= View::e($item['title']) ?></h1> <h1 id="page-title"><?= View::e($item['title']) ?></h1>
<?php if (!empty($item['summary'])): ?> <?php if (!empty($item['summary'])): ?>
<p><?= View::e($item['summary']) ?></p> <p><?= View::e($item['summary']) ?></p>
<?php endif; ?> <?php endif; ?>
@@ -136,7 +139,7 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
<?= $item['html'] ?> <?= $item['html'] ?>
</div> </div>
<?php if (!empty($item['tags'])): ?> <?php if (!empty($item['tags'])): ?>
<footer class="tag-row article-tags"> <footer class="tag-row article-tags" aria-label="Tags">
<?php foreach ($item['tags'] as $tag): ?> <?php foreach ($item['tags'] as $tag): ?>
<a href="<?= View::url('tag/' . ContentRepository::slugify((string) $tag)) ?>"><?= View::e($tag) ?></a> <a href="<?= View::url('tag/' . ContentRepository::slugify((string) $tag)) ?>"><?= View::e($tag) ?></a>
<?php endforeach; ?> <?php endforeach; ?>
@@ -148,7 +151,7 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
<section class="comments" id="comments"> <section class="comments" id="comments">
<h2>Comments</h2> <h2>Comments</h2>
<?php if (empty($approved_comments)): ?> <?php if (empty($approved_comments)): ?>
<p class="empty-state">No comments yet.</p> <p class="empty-state" role="status">No comments yet.</p>
<?php endif; ?> <?php endif; ?>
<?php foreach ($approved_comments as $comment): ?> <?php foreach ($approved_comments as $comment): ?>
<article class="comment"> <article class="comment">
@@ -162,15 +165,15 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
<form class="comment-form" action="<?= View::url('comment/' . $item['slug']) ?>" method="post"> <form class="comment-form" action="<?= View::url('comment/' . $item['slug']) ?>" method="post">
<div class="form-grid"> <div class="form-grid">
<label>Name <input name="author" required maxlength="120"></label> <label>Name <input name="author" required maxlength="120" autocomplete="name"></label>
<label>Email <input type="email" name="email" required maxlength="180"></label> <label>Email <input type="email" name="email" required maxlength="180" autocomplete="email"></label>
</div> </div>
<label>Website <input type="url" name="site_url" maxlength="220"></label> <label>Website <input type="url" name="site_url" maxlength="220" autocomplete="url"></label>
<label class="honey-field">Leave this empty <input name="<?= View::e($config->get('comments.honeypot_field', 'website_url')) ?>" tabindex="-1" autocomplete="off"></label> <label class="honey-field" aria-hidden="true">Leave this empty <input name="<?= View::e($config->get('comments.honeypot_field', 'website_url')) ?>" tabindex="-1" autocomplete="off"></label>
<label>Comment <textarea name="body" required rows="6" maxlength="5000"></textarea></label> <label>Comment <textarea name="body" required rows="6" maxlength="5000"></textarea></label>
<?php if ($config->get('comments.captcha', true)): ?> <?php if ($config->get('comments.captcha', true)): ?>
<input type="hidden" name="captcha_token" value="<?= View::e($captcha['token']) ?>"> <input type="hidden" name="captcha_token" value="<?= View::e($captcha['token']) ?>">
<label><?= View::e($captcha['question']) ?> <input name="captcha_answer" required inputmode="numeric"></label> <label>Captcha: <?= View::e($captcha['question']) ?> <input name="captcha_answer" required inputmode="numeric" autocomplete="off"></label>
<?php endif; ?> <?php endif; ?>
<button type="submit">Post Comment</button> <button type="submit">Post Comment</button>
</form> </form>
@@ -178,7 +181,7 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
<?php endif; ?> <?php endif; ?>
<?php else: ?> <?php else: ?>
<section class="error-view"> <section class="error-view">
<h1><?= View::e($title ?? 'Error') ?></h1> <h1 id="page-title"><?= View::e($title ?? 'Error') ?></h1>
<p><?= View::e($message ?? 'Something went wrong.') ?></p> <p><?= View::e($message ?? 'Something went wrong.') ?></p>
</section> </section>
<?php endif; ?> <?php endif; ?>
@@ -196,7 +199,7 @@ $renderPagination = static function (array $pagination, callable $url): void { ?
$socialUrl = View::url(ltrim($socialUrl, '/')); $socialUrl = View::url(ltrim($socialUrl, '/'));
} }
?> ?>
<a href="<?= View::e($socialUrl) ?>" rel="me noopener" target="_blank"><?= View::e($entry['label'] ?? $network) ?></a> <a href="<?= View::e($socialUrl) ?>" rel="me noopener" target="_blank"><?= View::e($entry['label'] ?? $network) ?><span class="sr-only"> opens in a new tab</span></a>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</section> </section>